Early work on tags
This commit is contained in:
parent
8cd9538a50
commit
0a04b25fe5
5 changed files with 520 additions and 9 deletions
190
bouquin/db.py
190
bouquin/db.py
|
|
@ -1,18 +1,29 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from sqlcipher3 import dbapi2 as sqlite
|
||||
from typing import List, Sequence, Tuple
|
||||
from typing import List, Sequence, Tuple, Iterable
|
||||
|
||||
|
||||
from . import strings
|
||||
|
||||
Entry = Tuple[str, str]
|
||||
TagRow = Tuple[int, str, str]
|
||||
|
||||
_TAG_COLORS = [
|
||||
"#FFB3BA", # soft red
|
||||
"#FFDFBA", # soft orange
|
||||
"#FFFFBA", # soft yellow
|
||||
"#BAFFC9", # soft green
|
||||
"#BAE1FF", # soft blue
|
||||
"#E0BAFF", # soft purple
|
||||
]
|
||||
|
||||
@dataclass
|
||||
class DBConfig:
|
||||
|
|
@ -82,7 +93,6 @@ class DBManager:
|
|||
# Always keep FKs on
|
||||
cur.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
# Create new versioned schema if missing (< 0.1.5)
|
||||
cur.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
|
|
@ -103,6 +113,24 @@ class DBManager:
|
|||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_versions_date_ver ON versions(date, version_no);
|
||||
CREATE INDEX IF NOT EXISTS ix_versions_date_created ON versions(date, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
color TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_tags_name ON tags(name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS page_tags (
|
||||
page_date TEXT NOT NULL, -- FK to pages.date
|
||||
tag_id INTEGER NOT NULL, -- FK to tags.id
|
||||
PRIMARY KEY (page_date, tag_id),
|
||||
FOREIGN KEY(page_date) REFERENCES pages(date) ON DELETE CASCADE,
|
||||
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_page_tags_tag_id ON page_tags(tag_id);
|
||||
"""
|
||||
)
|
||||
self.conn.commit()
|
||||
|
|
@ -142,25 +170,35 @@ class DBManager:
|
|||
|
||||
def search_entries(self, text: str) -> list[str]:
|
||||
"""
|
||||
Search for entries by term. This only works against the latest
|
||||
version of the page.
|
||||
Search for entries by term or tag name.
|
||||
This only works against the latest version of the page.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
pattern = f"%{text}%"
|
||||
q = text.strip()
|
||||
pattern = f"%{q.lower()}%"
|
||||
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT p.date, v.content
|
||||
SELECT DISTINCT p.date, v.content
|
||||
FROM pages AS p
|
||||
JOIN versions AS v
|
||||
ON v.id = p.current_version_id
|
||||
LEFT JOIN page_tags pt
|
||||
ON pt.page_date = p.date
|
||||
LEFT JOIN tags t
|
||||
ON t.id = pt.tag_id
|
||||
WHERE TRIM(v.content) <> ''
|
||||
AND v.content LIKE LOWER(?) ESCAPE '\\'
|
||||
AND (
|
||||
LOWER(v.content) LIKE ?
|
||||
OR LOWER(COALESCE(t.name, '')) LIKE ?
|
||||
)
|
||||
ORDER BY p.date DESC;
|
||||
""",
|
||||
(pattern,),
|
||||
(pattern, pattern),
|
||||
).fetchall()
|
||||
return [(r[0], r[1]) for r in rows]
|
||||
|
||||
|
||||
def dates_with_content(self) -> list[str]:
|
||||
"""
|
||||
Find all entries and return the dates of them.
|
||||
|
|
@ -386,6 +424,142 @@ class DBManager:
|
|||
except Exception as e:
|
||||
print(f"{strings._('error')}: {e}")
|
||||
|
||||
# -------- Tags: helpers -------------------------------------------
|
||||
|
||||
def _default_tag_colour(self, name: str) -> str:
|
||||
"""
|
||||
Deterministically pick a colour for a tag name from a small palette.
|
||||
"""
|
||||
if not name:
|
||||
return "#CCCCCC"
|
||||
h = int(hashlib.sha1(name.encode("utf-8")).hexdigest()[:8], 16)
|
||||
return _TAG_COLORS[h % len(_TAG_COLORS)]
|
||||
|
||||
# -------- Tags: per-page -------------------------------------------
|
||||
|
||||
def get_tags_for_page(self, date_iso: str) -> list[TagRow]:
|
||||
"""
|
||||
Return (id, name, color) for all tags attached to this page/date.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT t.id, t.name, t.color
|
||||
FROM page_tags pt
|
||||
JOIN tags t ON t.id = pt.tag_id
|
||||
WHERE pt.page_date = ?
|
||||
ORDER BY LOWER(t.name);
|
||||
""",
|
||||
(date_iso,),
|
||||
).fetchall()
|
||||
return [(r[0], r[1], r[2]) for r in rows]
|
||||
|
||||
def set_tags_for_page(self, date_iso: str, tag_names: Sequence[str]) -> None:
|
||||
"""
|
||||
Replace the tag set for a page with the given names.
|
||||
Creates new tags as needed (with auto colours).
|
||||
"""
|
||||
# Normalise + dedupe
|
||||
clean_names = []
|
||||
seen = set()
|
||||
for name in tag_names:
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
if name.lower() in seen:
|
||||
continue
|
||||
seen.add(name.lower())
|
||||
clean_names.append(name)
|
||||
|
||||
with self.conn:
|
||||
cur = self.conn.cursor()
|
||||
|
||||
# Ensure the page row exists even if there's no content yet
|
||||
cur.execute("INSERT OR IGNORE INTO pages(date) VALUES (?);", (date_iso,))
|
||||
|
||||
if not clean_names:
|
||||
# Just clear all tags for this page
|
||||
cur.execute("DELETE FROM page_tags WHERE page_date=?;", (date_iso,))
|
||||
return
|
||||
|
||||
# Ensure tag rows exist
|
||||
for name in clean_names:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO tags(name, color)
|
||||
VALUES (?, ?);
|
||||
""",
|
||||
(name, self._default_tag_colour(name)),
|
||||
)
|
||||
|
||||
# Lookup ids
|
||||
placeholders = ",".join("?" for _ in clean_names)
|
||||
rows = cur.execute(
|
||||
f"""
|
||||
SELECT id, name
|
||||
FROM tags
|
||||
WHERE name IN ({placeholders});
|
||||
""",
|
||||
tuple(clean_names),
|
||||
).fetchall()
|
||||
ids_by_name = {r["name"]: r["id"] for r in rows}
|
||||
|
||||
# Reset page_tags for this page
|
||||
cur.execute("DELETE FROM page_tags WHERE page_date=?;", (date_iso,))
|
||||
for name in clean_names:
|
||||
tag_id = ids_by_name.get(name)
|
||||
if tag_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO page_tags(page_date, tag_id)
|
||||
VALUES (?, ?);
|
||||
""",
|
||||
(date_iso, tag_id),
|
||||
)
|
||||
|
||||
# -------- Tags: global management ----------------------------------
|
||||
|
||||
def list_tags(self) -> list[TagRow]:
|
||||
"""
|
||||
Return all tags in the database.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT id, name, color
|
||||
FROM tags
|
||||
ORDER BY LOWER(name);
|
||||
"""
|
||||
).fetchall()
|
||||
return [(r[0], r[1], r[2]) for r in rows]
|
||||
|
||||
def update_tag(self, tag_id: int, name: str, color: str) -> None:
|
||||
"""
|
||||
Update a tag's name and colour.
|
||||
"""
|
||||
name = name.strip()
|
||||
color = color.strip() or "#CCCCCC"
|
||||
|
||||
with self.conn:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE tags
|
||||
SET name = ?, color = ?
|
||||
WHERE id = ?;
|
||||
""",
|
||||
(name, color, tag_id),
|
||||
)
|
||||
|
||||
def delete_tag(self, tag_id: int) -> None:
|
||||
"""
|
||||
Delete a tag entirely (removes it from all pages).
|
||||
"""
|
||||
with self.conn:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("DELETE FROM page_tags WHERE tag_id=?;", (tag_id,))
|
||||
cur.execute("DELETE FROM tags WHERE id=?;", (tag_id,))
|
||||
|
||||
def close(self) -> None:
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
|
|
|
|||
|
|
@ -110,5 +110,13 @@
|
|||
"toolbar_numbered_list": "Numbered list",
|
||||
"toolbar_code_block": "Code block",
|
||||
"toolbar_heading": "Heading",
|
||||
"toolbar_toggle_checkboxes": "Toggle checkboxes"
|
||||
"toolbar_toggle_checkboxes": "Toggle checkboxes",
|
||||
"tags": "Tags",
|
||||
"manage_tags": "Manage tags…",
|
||||
"add_tag_placeholder": "Add tag and press Enter…",
|
||||
"tag_name": "Tag name",
|
||||
"tag_color_hex": "Hex colour",
|
||||
"pick_color": "Pick colour…",
|
||||
"invalid_color_title": "Invalid colour",
|
||||
"invalid_color_message": "Please enter a valid hex colour like #RRGGBB."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ from .search import Search
|
|||
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
||||
from .settings_dialog import SettingsDialog
|
||||
from . import strings
|
||||
from .tags_widget import PageTagsWidget
|
||||
from .toolbar import ToolBar
|
||||
from .theme import ThemeManager
|
||||
|
||||
|
|
@ -93,6 +94,8 @@ class MainWindow(QMainWindow):
|
|||
self.search.openDateRequested.connect(self._load_selected_date)
|
||||
self.search.resultDatesChanged.connect(self._on_search_dates_changed)
|
||||
|
||||
self.tags = PageTagsWidget(self.db)
|
||||
|
||||
# Lock the calendar to the left panel at the top to stop it stretching
|
||||
# when the main window is resized.
|
||||
left_panel = QWidget()
|
||||
|
|
@ -100,6 +103,7 @@ class MainWindow(QMainWindow):
|
|||
left_layout.setContentsMargins(8, 8, 8, 8)
|
||||
left_layout.addWidget(self.calendar)
|
||||
left_layout.addWidget(self.search)
|
||||
left_layout.addWidget(self.tags)
|
||||
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
||||
|
||||
# Create tab widget to hold multiple editors
|
||||
|
|
@ -498,6 +502,12 @@ class MainWindow(QMainWindow):
|
|||
with QSignalBlocker(self.calendar):
|
||||
self.calendar.setSelectedDate(editor.current_date)
|
||||
|
||||
|
||||
# update per-page tags for the active tab
|
||||
if hasattr(self, "tags"):
|
||||
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||
self.tags.set_current_date(date_iso)
|
||||
|
||||
# Reconnect toolbar to new active editor
|
||||
self._sync_toolbar()
|
||||
|
||||
|
|
@ -628,6 +638,10 @@ class MainWindow(QMainWindow):
|
|||
# Keep tabs sorted by date
|
||||
self._reorder_tabs_by_date()
|
||||
|
||||
# sync tags
|
||||
if hasattr(self, "tags"):
|
||||
self.tags.set_current_date(date_iso)
|
||||
|
||||
def _load_date_into_editor(self, date: QDate, extra_data=False):
|
||||
"""Load a specific date's content into a given editor."""
|
||||
date_iso = date.toString("yyyy-MM-dd")
|
||||
|
|
|
|||
134
bouquin/tags_dialog.py
Normal file
134
bouquin/tags_dialog.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QPushButton,
|
||||
QColorDialog,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
from . import strings
|
||||
from .db import DBManager
|
||||
|
||||
class TagManagerDialog(QDialog):
|
||||
def __init__(self, db: DBManager, parent=None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
self.setWindowTitle(strings._("manage_tags"))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.table = QTableWidget(0, 2, self)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
[strings._("tag_name"), strings._("tag_color_hex")]
|
||||
)
|
||||
self.table.horizontalHeader().setStretchLastSection(True)
|
||||
layout.addWidget(self.table)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
self.add_btn = QPushButton(strings._("add"))
|
||||
self.remove_btn = QPushButton(strings._("remove"))
|
||||
self.color_btn = QPushButton(strings._("pick_color"))
|
||||
btn_row.addWidget(self.add_btn)
|
||||
btn_row.addWidget(self.remove_btn)
|
||||
btn_row.addWidget(self.color_btn)
|
||||
btn_row.addStretch(1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
action_row = QHBoxLayout()
|
||||
ok_btn = QPushButton(strings._("ok"))
|
||||
cancel_btn = QPushButton(strings._("cancel"))
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
action_row.addStretch(1)
|
||||
action_row.addWidget(ok_btn)
|
||||
action_row.addWidget(cancel_btn)
|
||||
layout.addLayout(action_row)
|
||||
|
||||
self.add_btn.clicked.connect(self._add_row)
|
||||
self.remove_btn.clicked.connect(self._remove_selected)
|
||||
self.color_btn.clicked.connect(self._pick_color)
|
||||
|
||||
self._load_tags()
|
||||
|
||||
def _load_tags(self) -> None:
|
||||
self.table.setRowCount(0)
|
||||
for tag_id, name, color in self._db.list_tags():
|
||||
row = self.table.rowCount()
|
||||
self.table.insertRow(row)
|
||||
|
||||
name_item = QTableWidgetItem(name)
|
||||
name_item.setData(Qt.ItemDataRole.UserRole, tag_id)
|
||||
self.table.setItem(row, 0, name_item)
|
||||
|
||||
color_item = QTableWidgetItem(color)
|
||||
color_item.setBackground(Qt.GlobalColor.transparent)
|
||||
self.table.setItem(row, 1, color_item)
|
||||
|
||||
def _add_row(self) -> None:
|
||||
row = self.table.rowCount()
|
||||
self.table.insertRow(row)
|
||||
name_item = QTableWidgetItem("")
|
||||
name_item.setData(Qt.ItemDataRole.UserRole, 0) # 0 => new tag
|
||||
self.table.setItem(row, 0, name_item)
|
||||
self.table.setItem(row, 1, QTableWidgetItem("#CCCCCC"))
|
||||
|
||||
def _remove_selected(self) -> None:
|
||||
row = self.table.currentRow()
|
||||
if row < 0:
|
||||
return
|
||||
item = self.table.item(row, 0)
|
||||
tag_id = item.data(Qt.ItemDataRole.UserRole)
|
||||
if tag_id:
|
||||
self._db.delete_tag(int(tag_id))
|
||||
self.table.removeRow(row)
|
||||
|
||||
def _pick_color(self) -> None:
|
||||
row = self.table.currentRow()
|
||||
if row < 0:
|
||||
return
|
||||
item = self.table.item(row, 1)
|
||||
current = item.text() or "#CCCCCC"
|
||||
color = QColorDialog.getColor()
|
||||
if color.isValid():
|
||||
item.setText(color.name())
|
||||
|
||||
def accept(self) -> None:
|
||||
# Persist all rows back to DB
|
||||
for row in range(self.table.rowCount()):
|
||||
name_item = self.table.item(row, 0)
|
||||
color_item = self.table.item(row, 1)
|
||||
if name_item is None or color_item is None:
|
||||
continue
|
||||
|
||||
name = name_item.text().strip()
|
||||
color = color_item.text().strip() or "#CCCCCC"
|
||||
tag_id = int(name_item.data(Qt.ItemDataRole.UserRole) or 0)
|
||||
|
||||
if not name:
|
||||
continue # ignore empty rows
|
||||
|
||||
if not color.startswith("#") or len(color) not in (4, 7):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
strings._("invalid_color_title"),
|
||||
strings._("invalid_color_message"),
|
||||
)
|
||||
return # keep dialog open
|
||||
|
||||
if tag_id == 0:
|
||||
# new tag: just rely on set_tags_for_page/create, or you can
|
||||
# insert here if you like. Easiest is to create via DBManager:
|
||||
# use a dummy page or do a direct insert
|
||||
self._db.set_tags_for_page("__dummy__", [name])
|
||||
# then delete the dummy row; or instead provide a DBManager
|
||||
# helper to create a tag explicitly.
|
||||
else:
|
||||
self._db.update_tag(tag_id, name, color)
|
||||
|
||||
super().accept()
|
||||
181
bouquin/tags_widget.py
Normal file
181
bouquin/tags_widget.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QToolButton,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QSizePolicy,
|
||||
QStyle,
|
||||
)
|
||||
|
||||
from . import strings
|
||||
from .db import DBManager
|
||||
|
||||
class TagChip(QFrame):
|
||||
removeRequested = Signal(int) # tag_id
|
||||
|
||||
def __init__(self, tag_id: int, name: str, color: str, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self._id = tag_id
|
||||
self.setObjectName("TagChip")
|
||||
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Raised)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(4, 0, 4, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
color_lbl = QLabel()
|
||||
color_lbl.setFixedSize(10, 10)
|
||||
color_lbl.setStyleSheet(f"background-color: {color}; border-radius: 3px;")
|
||||
layout.addWidget(color_lbl)
|
||||
|
||||
name_lbl = QLabel(name)
|
||||
layout.addWidget(name_lbl)
|
||||
|
||||
btn = QToolButton()
|
||||
btn.setText("×")
|
||||
btn.setAutoRaise(True)
|
||||
btn.clicked.connect(lambda: self.removeRequested.emit(self._id))
|
||||
layout.addWidget(btn)
|
||||
|
||||
@property
|
||||
def tag_id(self) -> int:
|
||||
return self._id
|
||||
|
||||
|
||||
class PageTagsWidget(QFrame):
|
||||
"""
|
||||
Collapsible per-page tag editor shown in the left sidebar.
|
||||
"""
|
||||
|
||||
def __init__(self, db: DBManager, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
self._current_date: Optional[str] = None
|
||||
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
|
||||
# Header (toggle + manage button)
|
||||
self.toggle_btn = QToolButton()
|
||||
self.toggle_btn.setText(strings._("tags"))
|
||||
self.toggle_btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
||||
self.toggle_btn.setCheckable(True)
|
||||
self.toggle_btn.setChecked(False)
|
||||
self.toggle_btn.setArrowType(Qt.RightArrow)
|
||||
self.toggle_btn.clicked.connect(self._on_toggle)
|
||||
|
||||
self.manage_btn = QToolButton()
|
||||
self.manage_btn.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
|
||||
self.manage_btn.setToolTip(strings._("manage_tags"))
|
||||
self.manage_btn.setAutoRaise(True)
|
||||
self.manage_btn.clicked.connect(self._open_manager)
|
||||
|
||||
header = QHBoxLayout()
|
||||
header.setContentsMargins(0, 0, 0, 0)
|
||||
header.addWidget(self.toggle_btn)
|
||||
header.addStretch(1)
|
||||
header.addWidget(self.manage_btn)
|
||||
|
||||
# Body (chips + add line)
|
||||
self.body = QWidget()
|
||||
self.body_layout = QVBoxLayout(self.body)
|
||||
self.body_layout.setContentsMargins(0, 4, 0, 0)
|
||||
self.body_layout.setSpacing(4)
|
||||
|
||||
# Simple horizontal layout for now; you can swap for a FlowLayout
|
||||
self.chip_row = QHBoxLayout()
|
||||
self.chip_row.setContentsMargins(0, 0, 0, 0)
|
||||
self.chip_row.setSpacing(4)
|
||||
self.body_layout.addLayout(self.chip_row)
|
||||
|
||||
self.add_edit = QLineEdit()
|
||||
self.add_edit.setPlaceholderText(strings._("add_tag_placeholder"))
|
||||
self.add_edit.returnPressed.connect(self._on_add_tag)
|
||||
self.body_layout.addWidget(self.add_edit)
|
||||
|
||||
self.body.setVisible(False)
|
||||
|
||||
main = QVBoxLayout(self)
|
||||
main.setContentsMargins(0, 0, 0, 0)
|
||||
main.addLayout(header)
|
||||
main.addWidget(self.body)
|
||||
|
||||
# ----- external API ------------------------------------------------
|
||||
|
||||
def set_current_date(self, date_iso: str) -> None:
|
||||
self._current_date = date_iso
|
||||
if self.toggle_btn.isChecked():
|
||||
self._reload_tags()
|
||||
else:
|
||||
# Keep it cheap while collapsed; reload only when expanded
|
||||
self._clear_chips()
|
||||
|
||||
# ----- internals ---------------------------------------------------
|
||||
|
||||
def _on_toggle(self, checked: bool) -> None:
|
||||
self.body.setVisible(checked)
|
||||
self.toggle_btn.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
||||
if checked and self._current_date:
|
||||
self._reload_tags()
|
||||
|
||||
def _clear_chips(self) -> None:
|
||||
while self.chip_row.count():
|
||||
item = self.chip_row.takeAt(0)
|
||||
w = item.widget()
|
||||
if w is not None:
|
||||
w.deleteLater()
|
||||
|
||||
def _reload_tags(self) -> None:
|
||||
if not self._current_date:
|
||||
self._clear_chips()
|
||||
return
|
||||
|
||||
self._clear_chips()
|
||||
tags = self._db.get_tags_for_page(self._current_date)
|
||||
for tag_id, name, color in tags:
|
||||
chip = TagChip(tag_id, name, color, self)
|
||||
chip.removeRequested.connect(self._remove_tag)
|
||||
self.chip_row.addWidget(chip)
|
||||
self.chip_row.addStretch(1)
|
||||
|
||||
def _on_add_tag(self) -> None:
|
||||
if not self._current_date:
|
||||
return
|
||||
new_tag = self.add_edit.text().strip()
|
||||
if not new_tag:
|
||||
return
|
||||
|
||||
# Combine current tags + new one, then write back
|
||||
existing = [name for _, name, _ in self._db.get_tags_for_page(self._current_date)]
|
||||
existing.append(new_tag)
|
||||
self._db.set_tags_for_page(self._current_date, existing)
|
||||
self.add_edit.clear()
|
||||
self._reload_tags()
|
||||
|
||||
def _remove_tag(self, tag_id: int) -> None:
|
||||
if not self._current_date:
|
||||
return
|
||||
tags = self._db.get_tags_for_page(self._current_date)
|
||||
remaining = [name for (tid, name, _color) in tags if tid != tag_id]
|
||||
self._db.set_tags_for_page(self._current_date, remaining)
|
||||
self._reload_tags()
|
||||
|
||||
def _open_manager(self) -> None:
|
||||
from .tags_dialog import TagManagerDialog
|
||||
|
||||
dlg = TagManagerDialog(self._db, self)
|
||||
if dlg.exec():
|
||||
# Names/colours may have changed
|
||||
if self._current_date:
|
||||
self._reload_tags()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue