from __future__ import annotations from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QComboBox, QPushButton, QCompleter, QWidget, QScrollArea, ) from PyQt6.QtCore import Qt from pathlib import Path from core.models import Article, ARTICLE_TYPES from core.frontmatter import parse_frontmatter, write_frontmatter from core.taxonomy import load_taxonomy class MultiTokenCompleter(QCompleter): """QCompleter that completes only the last comma-separated token.""" def __init__(self, tags: list[str], parent=None): super().__init__(tags, parent) self.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) def attach(self, line_edit): self.setWidget(line_edit) line_edit.textEdited.connect(self._on_text_edited) self.activated[str].connect(self._on_activated) def _on_text_edited(self, text: str): token = text.split(",")[-1].lstrip() if token: self.setCompletionPrefix(token) self.complete() else: self.popup().hide() def _on_activated(self, completion: str): widget = self.widget() if widget is None: return current = widget.text() if "," in current: prefix = current.rsplit(",", 1)[0] new_text = prefix + ", " + completion + ", " else: new_text = completion + ", " widget.setText(new_text) widget.setCursorPosition(len(new_text)) self.popup().hide() class FrontmatterEditor(QDialog): def __init__(self, article: Article, blog_root: Path, parent=None): super().__init__(parent) self.setWindowTitle(f"Frontmatter — {article.slug}") self.setMinimumWidth(520) self._article = article self._blog_root = blog_root self._fields: dict[str, QWidget] = {} self._build_ui() def _build_ui(self): layout = QVBoxLayout(self) scroll = QScrollArea() scroll.setWidgetResizable(True) inner = QWidget() form = QFormLayout(inner) scroll.setWidget(inner) layout.addWidget(scroll) lang = self._article.lang tags_it = self._blog_root / "docs" / "tags-it.txt" tags_en = self._blog_root / "docs" / "tags-en.txt" taxonomy = load_taxonomy(tags_it, tags_en) known_tags = list(taxonomy.it_to_en.keys()) if lang == "it" else list(taxonomy.it_to_en.values()) for key, val in self._article.frontmatter.items(): if key == "type": widget = QComboBox() widget.addItems(ARTICLE_TYPES) if str(val) in ARTICLE_TYPES: widget.setCurrentText(str(val)) self._fields[key] = widget elif key == "tags": widget = QLineEdit(", ".join(str(v) for v in val) if isinstance(val, list) else str(val)) MultiTokenCompleter(known_tags, widget).attach(widget) self._fields[key] = widget else: widget = QLineEdit(str(val)) self._fields[key] = widget form.addRow(QLabel(key), widget) btns = QHBoxLayout() save_btn = QPushButton("Salva") save_btn.clicked.connect(self._save) cancel_btn = QPushButton("Annulla") cancel_btn.clicked.connect(self.reject) btns.addStretch() btns.addWidget(cancel_btn) btns.addWidget(save_btn) layout.addLayout(btns) def _save(self): updated = dict(self._article.frontmatter) for key, widget in self._fields.items(): if isinstance(widget, QComboBox): updated[key] = widget.currentText() elif isinstance(widget, QLineEdit): val = widget.text().strip() if key == "tags": updated[key] = [t.strip() for t in val.split(",") if t.strip()] else: updated[key] = val try: _, body = parse_frontmatter(self._article.path) write_frontmatter(self._article.path, updated, body) self._article.frontmatter.update(updated) self.accept() except Exception as e: from PyQt6.QtWidgets import QMessageBox QMessageBox.critical(self, "Errore", str(e))