242 lines
8.7 KiB
Python
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)
|