# Lista de tareas

> Crea tu primera app de escritorio completa en Python con Pyzed: una lista de tareas con estado, formularios, listas, filtros y atajos de teclado.

Source: https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/

En este tutorial construimos una lista de tareas: la primera app clásica, y una buena, porque tiene todo lo que
tiene una app real: datos, un formulario para añadirlos, una lista para mostrarlos y formas de cambiarlos.

| Objetivo | Tiempo | Nivel | Necesitas |
| --- | --- | --- | --- |
| Una lista de tareas con filtros y atajos | 30 minutos | Básico · Python | [Tutorial en 5 pasos](https://albakore.github.io/pyzed/es/tutorials/basic/step-by-step/) (pasos 1 y 2) |

## Qué vas a aprender

- Guardar los datos de la app en objetos de Python normales (un dataclass).
- Leer un campo de texto mientras el usuario escribe y enviarlo con <kbd>Enter</kbd>.
- Dibujar una lista de filas, cada una con una casilla y un botón de borrar.
- Filtrar lo que se muestra y añadir atajos de teclado.

(Image: La app terminada)

1. **El modelo.** Una tarea es un dataclass. El estado de la app es la lista de tareas, el texto
   que se está escribiendo y el filtro activo:

```python
@dataclass
class Task:
    title: str
    done: bool = False

class Todo(pz.App):
    title = "Tareas"
    size = (520, 560)

    def __init__(self):
        self.tasks = [Task("Crear mi primera app")]
        self.draft = ""
        self.filter = "Todas"
```

2. **Añadir tareas.** El campo es *controlado*: muestra `self.draft` y cada pulsación lo
   actualiza. Enter (`on_submit`) y el botón llaman al mismo método:

```python
def add(self):
    if self.draft.strip():
        self.tasks.append(Task(self.draft.strip()))
        self.draft = ""
    return Command.focus("new-task")      # devuelve el foco al campo

# en view():
pz.row([
    ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
             placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"),
    ui.button("Añadir", self.add, variant="solid"),
], spacing=8)
```

3. **Dibujar la lista.** Cada tarea es un `ui.list_item` con una casilla al inicio y un botón de
   borrar al final. Las lambdas capturan `task` porque `task_row` recibe la tarea como argumento:

```python
def task_row(self, task):
    return ui.list_item(
        ui.label(task.title, color="text_muted" if task.done else "text"),
        lambda: self.toggle(task),
        start=ui.checkbox(task.done, lambda _: self.toggle(task)),
        end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"),
        height=34,
        inset=True,
    )
```

4. **Filtrar.** `ui.segmented` cambia `self.filter`, y `view()` solo muestra las tareas visibles.
   Como `view()` se vuelve a llamar después de cada cambio, no hay que "refrescar" nada:

```python
ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter, lambda f: setattr(self, "filter", f))
```

5. **Atajos.** `on_key` recibe cada tecla como texto:

```python
def on_key(self, combo):
    if combo == "ctrl-l":
        return self.clear_done()
    if combo == "ctrl-t":
        self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
```

6. **Foco inicial.** `on_start` se ejecuta una vez al abrir la ventana:

```python
def on_start(self):
    return Command.focus("new-task")
```

**Tip:**

Prueba a cambiar el tema con <kbd>Ctrl</kbd>+<kbd>T</kbd>: todos los componentes se restilizan
porque leen sus colores del tema activo.

## Código completo

`examples/todo.py`:

```py
"""A to-do list: state, text input, list items, filters and keyboard shortcuts.

    python examples/todo.py

Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/todo-app/
"""

from dataclasses import dataclass
from itertools import count

import pyzed as pz
from pyzed import Command, ui

_ids = count(1)

@dataclass
class Task:
    title: str
    done: bool = False
    id: int = 0

    def __post_init__(self):
        self.id = self.id or next(_ids)

class Todo(pz.App):
    title = "Tareas"
    size = (520, 560)
    min_size = (400, 360)

    def __init__(self):
        self.tasks = [Task("Leer la guía de Pyzed", done=True), Task("Crear mi primera app"), Task("Probar los temas")]
        self.draft = ""
        self.filter = "Todas"

    # -- actions -----------------------------------------------------------
    def add(self):
        if self.draft.strip():
            self.tasks.append(Task(self.draft.strip()))
            self.draft = ""
        return Command.focus("new-task")

    def toggle(self, task):
        task.done = not task.done

    def remove(self, task):
        self.tasks.remove(task)

    def clear_done(self):
        self.tasks = [t for t in self.tasks if not t.done]

    def on_start(self):
        return Command.focus("new-task")

    def on_key(self, combo):
        if combo == "ctrl-l":
            return self.clear_done()
        if combo == "ctrl-t":
            self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"

    # -- view ----------------------------------------------------------------
    def visible(self):
        if self.filter == "Pendientes":
            return [t for t in self.tasks if not t.done]
        if self.filter == "Hechas":
            return [t for t in self.tasks if t.done]
        return self.tasks

    def task_row(self, task):
        return ui.list_item(
            ui.label(task.title, color="text_muted" if task.done else "text"),
            lambda: self.toggle(task),
            start=ui.checkbox(task.done, lambda _: self.toggle(task)),
            end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"),
            height=34,
            inset=True,
        )

    def view(self):
        pending = sum(not t.done for t in self.tasks)
        tasks = self.visible()
        return pz.container(
            pz.column(
                [
                    pz.row([ui.heading("Tareas", level=2), pz.hspace(), ui.badge(f"{pending} pendientes")],
                           align="center"),
                    pz.row(
                        [
                            ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
                                     placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"),
                            ui.button("Añadir", self.add, variant="solid"),
                        ],
                        spacing=8,
                    ),
                    pz.row(
                        [
                            ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter,
                                         lambda f: setattr(self, "filter", f)),
                            pz.hspace(),
                            ui.button("Limpiar hechas", self.clear_done, variant="ghost", size="sm", keys="ctrl-l"),
                        ],
                        align="center",
                    ),
                    ui.card(
                        pz.scrollable(pz.column([self.task_row(t) for t in tasks], spacing=2), height="fill")
                        if tasks else pz.center(ui.muted("No hay tareas aquí."), height=120),
                        padding=6,
                    ),
                ],
                spacing=14,
                max_width=640,
            ),
            padding=24,
            width="fill",
            height="fill",
            align_x="center",
            bg=self.resolved_theme().editor,
        )

if __name__ == "__main__":
    pz.run(Todo)
```

## Eventos de este tutorial

| Widget | Evento | Handler | Recibe |
| --- | --- | --- | --- |
| `ui.input` | `on_input` | `lambda text: setattr(self, "draft", text)` | El texto. |
| `ui.input`, **Añadir** | `on_submit`, `on_press` | `self.add` | Nada. |
| `ui.segmented` | `on_select` | `lambda f: setattr(self, "filter", f)` | El filtro. |
| `ui.list_item` | `on_press` | `lambda: self.toggle(task)` | Nada. |
| `ui.checkbox` | `on_toggle` | `lambda _: self.toggle(task)` | `True`/`False` (se ignora: `_`). |
| `ui.icon_button` | `on_press` | `lambda: self.remove(task)` | Nada. |
| La app | `on_start`, `on_key` | enfoca el campo; `ctrl-l`, `ctrl-t` | —, la combinación. |

### Prueba más eventos

Renombra una tarea con doble clic y abre un menú con clic derecho:

```python
def __init__(self):
    ...
    self.renaming = None                    # la tarea que se renombra
    self.menu_for = None                    # la tarea con el menú abierto

def task_row(self, task):
    return ui.list_item(
        ui.label(task.title), lambda: self.toggle(task),
        start=ui.checkbox(task.done, lambda _: self.toggle(task)),
        on_double_press=lambda: setattr(self, "renaming", task),     # doble clic
        on_right_press=lambda: setattr(self, "menu_for", task),      # clic derecho
        height=34, inset=True,
    )

def view(self):
    page = ...                              # la página del tutorial
    dialog = None
    if self.renaming:
        task = self.renaming
        dialog = ui.prompt("Renombrar", task.title, lambda t: setattr(task, "title", t),
                           lambda: setattr(self, "renaming", None), lambda: setattr(self, "renaming", None))
    return ui.dialog_layer(page, dialog)
```

## Siguiente

- [Enlace de datos y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/) en el diseñador.
- [Listas y tablas](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/): buscar, ordenar y seleccionar filas.
- La [app Todos del Showcase](https://albakore.github.io/pyzed/es/showcase/apps/#todos), que guarda la lista en disco.
