Files
Medios-Macina/TUI/modalscreen/matrix_room_picker.py
2026-01-31 19:00:04 -08:00

242 lines
8.7 KiB
Python

from __future__ import annotations
from typing import Any, Dict, List, Optional
from textual.app import ComposeResult
from textual.containers import Container, Horizontal, ScrollableContainer
from textual.screen import ModalScreen
from textual.widgets import Static, Button, Checkbox, ListView, ListItem
from textual import work
from rich.text import Text
import logging
logger = logging.getLogger(__name__)
class MatrixRoomPicker(ModalScreen[List[str]]):
"""Modal that lists Matrix rooms and returns selected defaults."""
CSS = """
MatrixRoomPicker {
align: center middle;
background: rgba(0, 0, 0, 0.45);
}
#matrix-room-picker {
width: 70%;
height: 70%;
background: $panel;
border: thick $primary;
padding: 1;
}
#matrix-room-picker-hint {
text-style: italic;
color: $text-muted;
margin-bottom: 1;
}
#matrix-room-scroll {
height: 1fr;
border: solid $surface;
padding: 1;
margin-bottom: 1;
}
#matrix-room-list {
padding: 0;
}
.matrix-room-row {
border-bottom: solid $surface;
padding: 1 0;
align: left middle;
}
.matrix-room-meta {
padding-left: 1;
content-align: left middle;
}
#matrix-room-actions {
height: 3;
align: right middle;
}
#matrix-room-status {
height: 3;
text-style: bold;
}
"""
def __init__(
self,
config: Dict[str, Any],
*,
existing: Optional[List[str]] = None,
rooms: Optional[List[Dict[str, Any]]] = None,
) -> None:
super().__init__()
self.config = config
self._prefetched_rooms = rooms
self._existing_ids = {str(r).strip() for r in (existing or []) if str(r).strip()}
self._checkbox_map: Dict[str, str] = {}
self._rooms: List[Dict[str, Any]] = []
self._status_widget: Optional[Static] = None
self._checklist: Optional[Vertical] = None
self._save_button: Optional[Button] = None
def compose(self) -> ComposeResult:
with Container(id="matrix-room-picker"):
yield Static("Matrix Default Rooms", classes="section-title")
yield Static(
"Choose rooms to keep in the sharing defaults and autocomplete.",
id="matrix-room-picker-hint",
)
with ScrollableContainer(id="matrix-room-scroll"):
yield ListView(id="matrix-room-list")
with Horizontal(id="matrix-room-actions"):
yield Button("Cancel", variant="error", id="matrix-room-cancel")
yield Button("Select All", id="matrix-room-select-all")
yield Button("Clear All", id="matrix-room-clear")
yield Button("Save defaults", variant="success", id="matrix-room-save")
yield Static("Loading rooms...", id="matrix-room-status")
def on_mount(self) -> None:
self._status_widget = self.query_one("#matrix-room-status", Static)
self._checklist = self.query_one("#matrix-room-list", ListView)
self._save_button = self.query_one("#matrix-room-save", Button)
if self._save_button:
self._save_button.disabled = True
if self._prefetched_rooms is not None:
self._apply_room_results(self._prefetched_rooms, None)
else:
if self._status_widget:
self._set_status("Loading rooms...")
self._load_rooms_background()
def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Intercept ListView.Selected events and prevent them from bubbling up
to parent components which may react (e.g., the main config modal).
Selecting an item should not implicitly close the picker or change the
outer editor state."""
try:
# Stop propagation so parent handlers (ConfigModal) don't react.
event.stop()
except Exception:
logger.exception("Failed to stop ListView.Selected event propagation")
def _set_status(self, text: str) -> None:
if self._status_widget:
self._status_widget.update(text)
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
# Enable Save only when at least one checkbox is selected
any_selected = False
for checkbox_id in self._checkbox_map.keys():
try:
cb = self.query_one(f"#{checkbox_id}", Checkbox)
if cb.value:
any_selected = True
break
except Exception:
logger.exception("Error querying checkbox in MatrixRoomPicker; skipping")
continue
if self._save_button:
self._save_button.disabled = not any_selected
def _render_rooms(self, rooms: List[Dict[str, Any]]) -> None:
if not self._checklist:
return
for child in list(self._checklist.children):
child.remove()
self._checkbox_map.clear()
self._rooms = rooms
if not rooms:
self._set_status("Matrix returned no rooms.")
if self._save_button:
self._save_button.disabled = True
return
for idx, room in enumerate(rooms):
room_id = str(room.get("room_id") or "").strip()
name = str(room.get("name") or "").strip()
checkbox_id = f"matrix-room-{idx}"
# Prefer display name; otherwise fall back to room id
label_text = name or room_id or "Matrix Room"
checkbox = Checkbox(
label_text,
id=checkbox_id,
value=bool(room_id and room_id in self._existing_ids),
)
self._checkbox_map[checkbox_id] = room_id
list_item = ListItem(checkbox, classes="matrix-room-row")
self._checklist.mount(list_item)
self._set_status("Loaded rooms. Select one or more and save.")
if self._save_button:
self._save_button.disabled = False
@work(thread=True)
def _load_rooms_background(self) -> None:
try:
from Provider.matrix import Matrix
provider = Matrix(self.config)
rooms = provider.list_rooms()
self.app.call_from_thread(self._apply_room_results, rooms, None)
except Exception as exc:
self.app.call_from_thread(self._apply_room_results, [], str(exc))
def _apply_room_results(self, rooms: List[Dict[str, Any]], error: Optional[str]) -> None:
if error:
self._set_status(f"Failed to load Matrix rooms: {error}")
if self._save_button:
self._save_button.disabled = True
return
self._render_rooms(rooms)
# Ensure save button is enabled only if at least one checkbox is selected
any_selected = False
for cbid in self._checkbox_map.keys():
try:
cb = self.query_one(f"#{cbid}", Checkbox)
if cb.value:
any_selected = True
break
except Exception:
continue
if self._save_button:
self._save_button.disabled = not any_selected
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "matrix-room-cancel":
self.dismiss([])
elif event.button.id == "matrix-room-select-all":
for checkbox_id in self._checkbox_map.keys():
try:
cb = self.query_one(f"#{checkbox_id}", Checkbox)
cb.value = True
except Exception:
logger.exception("Failed to set checkbox value in MatrixRoomPicker")
if self._save_button:
self._save_button.disabled = False
elif event.button.id == "matrix-room-clear":
for checkbox_id in self._checkbox_map.keys():
try:
cb = self.query_one(f"#{checkbox_id}", Checkbox)
cb.value = False
except Exception:
logger.exception("Failed to set checkbox value to False in MatrixRoomPicker")
if self._save_button:
self._save_button.disabled = True
elif event.button.id == "matrix-room-save":
selected: List[str] = []
for checkbox_id, room_id in self._checkbox_map.items():
try:
cb = self.query_one(f"#{checkbox_id}", Checkbox)
if cb.value and room_id:
selected.append(room_id)
except Exception:
logger.exception("Failed to read checkbox state for '%s' while saving MatrixRoomPicker selection", checkbox_id)
self.dismiss(selected)