Tags working
This commit is contained in:
parent
3263788415
commit
f6e10dccac
11 changed files with 1148 additions and 267 deletions
|
|
@ -8,7 +8,7 @@ import json
|
|||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from sqlcipher3 import dbapi2 as sqlite
|
||||
from typing import List, Sequence, Tuple, Iterable
|
||||
from typing import List, Sequence, Tuple
|
||||
|
||||
|
||||
from . import strings
|
||||
|
|
@ -458,8 +458,9 @@ class DBManager:
|
|||
"""
|
||||
Replace the tag set for a page with the given names.
|
||||
Creates new tags as needed (with auto colours).
|
||||
Tags are case-insensitive - reuses existing tag if found with different case.
|
||||
"""
|
||||
# Normalise + dedupe
|
||||
# Normalise + dedupe (case-insensitive)
|
||||
clean_names = []
|
||||
seen = set()
|
||||
for name in tag_names:
|
||||
|
|
@ -482,31 +483,44 @@ class DBManager:
|
|||
cur.execute("DELETE FROM page_tags WHERE page_date=?;", (date_iso,))
|
||||
return
|
||||
|
||||
# Ensure tag rows exist
|
||||
# For each tag name, check if it exists with different casing
|
||||
# If so, reuse that existing tag; otherwise create new
|
||||
final_tag_names = []
|
||||
for name in clean_names:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO tags(name, color)
|
||||
VALUES (?, ?);
|
||||
""",
|
||||
(name, self._default_tag_colour(name)),
|
||||
)
|
||||
# Look for existing tag (case-insensitive)
|
||||
existing = cur.execute(
|
||||
"SELECT name FROM tags WHERE LOWER(name) = LOWER(?);", (name,)
|
||||
).fetchone()
|
||||
|
||||
# Lookup ids
|
||||
placeholders = ",".join("?" for _ in clean_names)
|
||||
if existing:
|
||||
# Use the existing tag's exact name
|
||||
final_tag_names.append(existing["name"])
|
||||
else:
|
||||
# Create new tag with the provided casing
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO tags(name, color)
|
||||
VALUES (?, ?);
|
||||
""",
|
||||
(name, self._default_tag_colour(name)),
|
||||
)
|
||||
final_tag_names.append(name)
|
||||
|
||||
# Lookup ids for the final tag names
|
||||
placeholders = ",".join("?" for _ in final_tag_names)
|
||||
rows = cur.execute(
|
||||
f"""
|
||||
SELECT id, name
|
||||
FROM tags
|
||||
WHERE name IN ({placeholders});
|
||||
""",
|
||||
tuple(clean_names),
|
||||
tuple(final_tag_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:
|
||||
for name in final_tag_names:
|
||||
tag_id = ids_by_name.get(name)
|
||||
if tag_id is not None:
|
||||
cur.execute(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QPoint, QRect, QSize, Qt
|
||||
from PySide6.QtWidgets import QLayout, QSizePolicy
|
||||
from PySide6.QtWidgets import QLayout
|
||||
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
|
|
|
|||
|
|
@ -112,15 +112,23 @@
|
|||
"toolbar_heading": "Heading",
|
||||
"toolbar_toggle_checkboxes": "Toggle checkboxes",
|
||||
"tags": "Tags",
|
||||
"manage_tags": "Manage tags on this page",
|
||||
"manage_tags": "Manage tags",
|
||||
"add_tag_placeholder": "Add a tag and press Enter",
|
||||
"tag_browser_title": "Tag Browser",
|
||||
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
|
||||
"tag_name": "Tag name",
|
||||
"tag_color_hex": "Hex colour",
|
||||
"color_hex": "Color",
|
||||
"date": "Date",
|
||||
"pick_color": "Pick colour",
|
||||
"invalid_color_title": "Invalid colour",
|
||||
"invalid_color_message": "Please enter a valid hex colour like #RRGGBB.",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"edit_tag_name": "Edit tag name",
|
||||
"new_tag_name": "New tag name:",
|
||||
"change_color": "Change colour",
|
||||
"delete_tag": "Delete tag",
|
||||
"delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,5 +110,25 @@
|
|||
"toolbar_numbered_list": "Liste numérotée",
|
||||
"toolbar_code_block": "Bloc de code",
|
||||
"toolbar_heading": "Titre",
|
||||
"toolbar_toggle_checkboxes": "Cocher/Décocher les cases"
|
||||
"toolbar_toggle_checkboxes": "Cocher/Décocher les cases",
|
||||
"tags": "Tags",
|
||||
"manage_tags": "Gérer les tags",
|
||||
"add_tag_placeholder": "Ajouter un tag et appuyez sur Entrée",
|
||||
"tag_browser_title": "Navigateur de tags",
|
||||
"tag_browser_instructions": "Cliquez sur un tag pour l'étendre et voir toutes les pages avec ce tag. Cliquez sur une date pour l'ouvrir. Sélectionnez un tag pour modifier son nom, changer sa couleur ou le supprimer globalement.",
|
||||
"tag_name": "Nom du tag",
|
||||
"tag_color_hex": "Couleur hexadécimale",
|
||||
"color_hex": "Couleur",
|
||||
"date": "Date",
|
||||
"pick_color": "Choisir la couleur",
|
||||
"invalid_color_title": "Couleur invalide",
|
||||
"invalid_color_message": "Veuillez entrer une couleur hexadécimale valide comme #RRGGBB.",
|
||||
"add": "Ajouter",
|
||||
"remove": "Supprimer",
|
||||
"ok": "OK",
|
||||
"edit_tag_name": "Modifier le nom du tag",
|
||||
"new_tag_name": "Nouveau nom du tag :",
|
||||
"change_color": "Changer la couleur",
|
||||
"delete_tag": "Supprimer le tag",
|
||||
"delete_tag_confirm": "Êtes-vous sûr de vouloir supprimer le tag '{name}' ? Cela le supprimera de toutes les pages."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,5 +110,25 @@
|
|||
"toolbar_numbered_list": "Elenco numerato",
|
||||
"toolbar_code_block": "Blocco di codice",
|
||||
"toolbar_heading": "Titolo",
|
||||
"toolbar_toggle_checkboxes": "Attiva/disattiva caselle di controllo"
|
||||
"toolbar_toggle_checkboxes": "Attiva/disattiva caselle di controllo",
|
||||
"tags": "Tag",
|
||||
"manage_tags": "Gestisci tag",
|
||||
"add_tag_placeholder": "Aggiungi un tag e premi Invio",
|
||||
"tag_browser_title": "Browser dei tag",
|
||||
"tag_browser_instructions": "Fai clic su un tag per espandere e vedere tutte le pagine con quel tag. Fai clic su una data per aprirla. Seleziona un tag per modificarne il nome, cambiarne il colore o eliminarlo globalmente.",
|
||||
"tag_name": "Nome del tag",
|
||||
"tag_color_hex": "Colore esadecimale",
|
||||
"color_hex": "Colore",
|
||||
"date": "Data",
|
||||
"pick_color": "Scegli colore",
|
||||
"invalid_color_title": "Colore non valido",
|
||||
"invalid_color_message": "Inserisci un colore esadecimale valido come #RRGGBB.",
|
||||
"add": "Aggiungi",
|
||||
"remove": "Rimuovi",
|
||||
"ok": "OK",
|
||||
"edit_tag_name": "Modifica nome tag",
|
||||
"new_tag_name": "Nuovo nome tag:",
|
||||
"change_color": "Cambia colore",
|
||||
"delete_tag": "Elimina tag",
|
||||
"delete_tag_confirm": "Sei sicuro di voler eliminare il tag '{name}'? Questo lo rimuoverà da tutte le pagine."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ 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 .status_tags_widget import StatusBarTagsWidget
|
||||
from .toolbar import ToolBar
|
||||
from .theme import ThemeManager
|
||||
|
||||
|
|
@ -96,6 +95,8 @@ class MainWindow(QMainWindow):
|
|||
self.search.resultDatesChanged.connect(self._on_search_dates_changed)
|
||||
|
||||
self.tags = PageTagsWidget(self.db)
|
||||
self.tags.tagActivated.connect(self._on_tag_activated)
|
||||
self.tags.tagAdded.connect(self._on_tag_added)
|
||||
|
||||
# Lock the calendar to the left panel at the top to stop it stretching
|
||||
# when the main window is resized.
|
||||
|
|
@ -181,11 +182,6 @@ class MainWindow(QMainWindow):
|
|||
# FindBar will get the current editor dynamically via a callable
|
||||
self.findBar = FindBar(lambda: self.editor, shortcut_parent=self, parent=self)
|
||||
self.statusBar().addPermanentWidget(self.findBar)
|
||||
# status-bar tags widget
|
||||
self.statusTags = StatusBarTagsWidget(self.db, self)
|
||||
self.statusBar().addPermanentWidget(self.statusTags)
|
||||
# Clicking a tag in the status bar will open tag pages (next section)
|
||||
self.statusTags.tagActivated.connect(self._on_tag_activated)
|
||||
# When the findBar closes, put the caret back in the editor
|
||||
self.findBar.closed.connect(self._focus_editor_now)
|
||||
|
||||
|
|
@ -213,6 +209,10 @@ class MainWindow(QMainWindow):
|
|||
act_backup.setShortcut("Ctrl+Shift+B")
|
||||
act_backup.triggered.connect(self._backup)
|
||||
file_menu.addAction(act_backup)
|
||||
act_tags = QAction("&" + strings._("manage_tags"), self)
|
||||
act_tags.setShortcut("Ctrl+T")
|
||||
act_tags.triggered.connect(self.tags._open_manager)
|
||||
file_menu.addAction(act_tags)
|
||||
file_menu.addSeparator()
|
||||
act_quit = QAction("&" + strings._("quit"), self)
|
||||
act_quit.setShortcut("Ctrl+Q")
|
||||
|
|
@ -509,6 +509,7 @@ class MainWindow(QMainWindow):
|
|||
self.calendar.setSelectedDate(editor.current_date)
|
||||
|
||||
# update per-page tags for the active tab
|
||||
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||
self._update_tag_views_for_date(date_iso)
|
||||
|
||||
# Reconnect toolbar to new active editor
|
||||
|
|
@ -814,6 +815,10 @@ class MainWindow(QMainWindow):
|
|||
if current_index >= 0:
|
||||
self.tab_widget.setTabText(current_index, new_date.toString("yyyy-MM-dd"))
|
||||
|
||||
# Update tags for the newly loaded page
|
||||
date_iso = new_date.toString("yyyy-MM-dd")
|
||||
self._update_tag_views_for_date(date_iso)
|
||||
|
||||
# Keep tabs sorted by date
|
||||
self._reorder_tabs_by_date()
|
||||
|
||||
|
|
@ -1034,15 +1039,45 @@ class MainWindow(QMainWindow):
|
|||
def _update_tag_views_for_date(self, date_iso: str):
|
||||
if hasattr(self, "tags"):
|
||||
self.tags.set_current_date(date_iso)
|
||||
if hasattr(self, "statusTags"):
|
||||
self.statusTags.set_current_page(date_iso)
|
||||
|
||||
def _on_tag_activated(self, tag_name: str):
|
||||
from .tag_browser import TagBrowserDialog
|
||||
def _on_tag_added(self):
|
||||
"""Called when a tag is added - trigger autosave for current page"""
|
||||
# Use QTimer to defer the save slightly, avoiding re-entrancy issues
|
||||
from PySide6.QtCore import QTimer
|
||||
|
||||
dlg = TagBrowserDialog(self.db, self, focus_tag=tag_name)
|
||||
dlg.openDateRequested.connect(self._load_selected_date)
|
||||
dlg.exec()
|
||||
QTimer.singleShot(0, self._do_tag_save)
|
||||
|
||||
def _do_tag_save(self):
|
||||
"""Actually perform the save after tag is added"""
|
||||
if hasattr(self, "editor") and hasattr(self.editor, "current_date"):
|
||||
date_iso = self.editor.current_date.toString("yyyy-MM-dd")
|
||||
|
||||
# Get current editor content
|
||||
text = self.editor.to_markdown()
|
||||
|
||||
# Save the content (or blank if page is empty)
|
||||
# This ensures the page shows up in tag browser
|
||||
self.db.save_new_version(date_iso, text, note="Tag added")
|
||||
self._dirty = False
|
||||
self._refresh_calendar_marks()
|
||||
from datetime import datetime as _dt
|
||||
|
||||
self.statusBar().showMessage(
|
||||
strings._("saved") + f" {date_iso}: {_dt.now().strftime('%H:%M:%S')}",
|
||||
2000,
|
||||
)
|
||||
|
||||
def _on_tag_activated(self, tag_name_or_date: str):
|
||||
# If it's a date (YYYY-MM-DD format), load it
|
||||
if len(tag_name_or_date) == 10 and tag_name_or_date.count("-") == 2:
|
||||
self._load_selected_date(tag_name_or_date)
|
||||
else:
|
||||
# It's a tag name, open the tag browser
|
||||
from .tag_browser import TagBrowserDialog
|
||||
|
||||
dlg = TagBrowserDialog(self.db, self, focus_tag=tag_name_or_date)
|
||||
dlg.openDateRequested.connect(self._load_selected_date)
|
||||
dlg.exec()
|
||||
|
||||
# ----------- Settings handler ------------#
|
||||
def _open_settings(self):
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
from .flow_layout import FlowLayout
|
||||
from .db import DBManager
|
||||
from .tags_widget import TagChip # reuse, or make a smaller variant if you prefer
|
||||
|
||||
|
||||
class StatusBarTagsWidget(QWidget):
|
||||
tagActivated = Signal(str) # tag name
|
||||
|
||||
def __init__(self, db: DBManager, parent=None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
self._current_date_iso: str | None = None
|
||||
|
||||
outer = QHBoxLayout(self)
|
||||
outer.setContentsMargins(4, 0, 4, 0)
|
||||
outer.setSpacing(4)
|
||||
|
||||
label = QLabel("Tags:")
|
||||
outer.addWidget(label)
|
||||
|
||||
self.flow = FlowLayout(self, hspacing=2, vspacing=2)
|
||||
outer.addLayout(self.flow)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
|
||||
def set_current_page(self, date_iso: str):
|
||||
self._current_date_iso = date_iso
|
||||
self._reload()
|
||||
|
||||
def _clear(self):
|
||||
while self.flow.count():
|
||||
item = self.flow.takeAt(0)
|
||||
w = item.widget()
|
||||
if w is not None:
|
||||
w.deleteLater()
|
||||
|
||||
def _reload(self):
|
||||
self._clear()
|
||||
if not self._current_date_iso:
|
||||
return
|
||||
|
||||
tags = self._db.get_tags_for_page(self._current_date_iso)
|
||||
# Keep it small; maybe only first N tags:
|
||||
MAX_TAGS = 6
|
||||
for i, (tid, name, color) in enumerate(tags):
|
||||
if i >= MAX_TAGS:
|
||||
more = QLabel("…")
|
||||
self.flow.addWidget(more)
|
||||
break
|
||||
chip = TagChip(tid, name, color, self)
|
||||
chip.clicked.connect(self.tagActivated)
|
||||
# In status bar you might want a smaller style: adjust chip's stylesheet if needed.
|
||||
self.flow.addWidget(chip)
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
# tag_browser.py
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QPushButton,
|
||||
QLabel,
|
||||
QColorDialog,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
from .db import DBManager
|
||||
|
|
@ -18,33 +23,86 @@ class TagBrowserDialog(QDialog):
|
|||
def __init__(self, db: DBManager, parent=None, focus_tag: str | None = None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
self.setWindowTitle(strings._("tag_browser_title"))
|
||||
self.setWindowTitle(
|
||||
strings._("tag_browser_title") + " / " + strings._("manage_tags")
|
||||
)
|
||||
self.resize(600, 500)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Instructions
|
||||
instructions = QLabel(strings._("tag_browser_instructions"))
|
||||
instructions.setWordWrap(True)
|
||||
layout.addWidget(instructions)
|
||||
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabels([strings._("tag"), strings._("date")])
|
||||
self.tree.setHeaderLabels(
|
||||
[strings._("tag"), strings._("color_hex"), strings._("date")]
|
||||
)
|
||||
self.tree.setColumnWidth(0, 200)
|
||||
self.tree.setColumnWidth(1, 100)
|
||||
self.tree.itemActivated.connect(self._on_item_activated)
|
||||
self.tree.itemClicked.connect(self._on_item_clicked)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
# Tag management buttons
|
||||
btn_row = QHBoxLayout()
|
||||
|
||||
self.edit_name_btn = QPushButton(strings._("edit_tag_name"))
|
||||
self.edit_name_btn.clicked.connect(self._edit_tag_name)
|
||||
self.edit_name_btn.setEnabled(False)
|
||||
btn_row.addWidget(self.edit_name_btn)
|
||||
|
||||
self.change_color_btn = QPushButton(strings._("change_color"))
|
||||
self.change_color_btn.clicked.connect(self._change_tag_color)
|
||||
self.change_color_btn.setEnabled(False)
|
||||
btn_row.addWidget(self.change_color_btn)
|
||||
|
||||
self.delete_btn = QPushButton(strings._("delete_tag"))
|
||||
self.delete_btn.clicked.connect(self._delete_tag)
|
||||
self.delete_btn.setEnabled(False)
|
||||
btn_row.addWidget(self.delete_btn)
|
||||
|
||||
btn_row.addStretch(1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
# Close button
|
||||
close_row = QHBoxLayout()
|
||||
close_row.addStretch(1)
|
||||
close_btn = QPushButton(strings._("close"))
|
||||
close_btn.clicked.connect(self.accept)
|
||||
layout.addWidget(close_btn)
|
||||
close_row.addWidget(close_btn)
|
||||
layout.addLayout(close_row)
|
||||
|
||||
self._populate(focus_tag)
|
||||
|
||||
def _populate(self, focus_tag: str | None):
|
||||
self.tree.clear()
|
||||
tags = self._db.list_tags()
|
||||
focus_item = None
|
||||
|
||||
for tag_id, name, color in tags:
|
||||
root = QTreeWidgetItem([name, ""])
|
||||
# coloured background or icon:
|
||||
root.setData(0, Qt.ItemDataRole.UserRole, name)
|
||||
# Create the tree item
|
||||
root = QTreeWidgetItem([name, "", ""])
|
||||
root.setData(
|
||||
0,
|
||||
Qt.ItemDataRole.UserRole,
|
||||
{"type": "tag", "id": tag_id, "name": name, "color": color},
|
||||
)
|
||||
|
||||
# Set background color for the second column to show the tag color
|
||||
root.setBackground(1, QColor(color))
|
||||
root.setText(1, color) # Also show the hex code
|
||||
root.setTextAlignment(1, Qt.AlignCenter)
|
||||
|
||||
self.tree.addTopLevelItem(root)
|
||||
|
||||
pages = self._db.get_pages_for_tag(name)
|
||||
for date_iso, _content in pages:
|
||||
child = QTreeWidgetItem(["", date_iso])
|
||||
child.setData(0, Qt.ItemDataRole.UserRole, date_iso)
|
||||
child = QTreeWidgetItem(["", "", date_iso])
|
||||
child.setData(
|
||||
0, Qt.ItemDataRole.UserRole, {"type": "page", "date": date_iso}
|
||||
)
|
||||
root.addChild(child)
|
||||
|
||||
if focus_tag and name.lower() == focus_tag.lower():
|
||||
|
|
@ -54,7 +112,94 @@ class TagBrowserDialog(QDialog):
|
|||
self.tree.expandItem(focus_item)
|
||||
self.tree.setCurrentItem(focus_item)
|
||||
|
||||
def _on_item_clicked(self, item: QTreeWidgetItem, column: int):
|
||||
"""Enable/disable buttons based on selection"""
|
||||
data = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if isinstance(data, dict):
|
||||
if data.get("type") == "tag":
|
||||
self.edit_name_btn.setEnabled(True)
|
||||
self.change_color_btn.setEnabled(True)
|
||||
self.delete_btn.setEnabled(True)
|
||||
else:
|
||||
self.edit_name_btn.setEnabled(False)
|
||||
self.change_color_btn.setEnabled(False)
|
||||
self.delete_btn.setEnabled(False)
|
||||
|
||||
def _on_item_activated(self, item: QTreeWidgetItem, column: int):
|
||||
date_iso = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if isinstance(date_iso, str) and date_iso:
|
||||
self.openDateRequested.emit(date_iso)
|
||||
data = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if isinstance(data, dict):
|
||||
if data.get("type") == "page":
|
||||
date_iso = data.get("date")
|
||||
if date_iso:
|
||||
self.openDateRequested.emit(date_iso)
|
||||
self.accept()
|
||||
|
||||
def _edit_tag_name(self):
|
||||
"""Edit the name of the selected tag"""
|
||||
item = self.tree.currentItem()
|
||||
if not item:
|
||||
return
|
||||
|
||||
data = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(data, dict) or data.get("type") != "tag":
|
||||
return
|
||||
|
||||
tag_id = data["id"]
|
||||
old_name = data["name"]
|
||||
color = data["color"]
|
||||
|
||||
# Simple input dialog
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
new_name, ok = QInputDialog.getText(
|
||||
self, strings._("edit_tag_name"), strings._("new_tag_name"), text=old_name
|
||||
)
|
||||
|
||||
if ok and new_name and new_name != old_name:
|
||||
self._db.update_tag(tag_id, new_name, color)
|
||||
self._populate(None)
|
||||
|
||||
def _change_tag_color(self):
|
||||
"""Change the color of the selected tag"""
|
||||
item = self.tree.currentItem()
|
||||
if not item:
|
||||
return
|
||||
|
||||
data = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(data, dict) or data.get("type") != "tag":
|
||||
return
|
||||
|
||||
tag_id = data["id"]
|
||||
name = data["name"]
|
||||
current_color = data["color"]
|
||||
|
||||
color = QColorDialog.getColor(QColor(current_color), self)
|
||||
if color.isValid():
|
||||
self._db.update_tag(tag_id, name, color.name())
|
||||
self._populate(None)
|
||||
|
||||
def _delete_tag(self):
|
||||
"""Delete the selected tag"""
|
||||
item = self.tree.currentItem()
|
||||
if not item:
|
||||
return
|
||||
|
||||
data = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(data, dict) or data.get("type") != "tag":
|
||||
return
|
||||
|
||||
tag_id = data["id"]
|
||||
name = data["name"]
|
||||
|
||||
# Confirm deletion
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
strings._("delete_tag"),
|
||||
strings._("delete_tag_confirm").format(name=name),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
self._db.delete_tag(tag_id)
|
||||
self._populate(None)
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
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"))
|
||||
close_btn = QPushButton(strings._("close"))
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
close_btn.clicked.connect(self.reject)
|
||||
action_row.addStretch(1)
|
||||
action_row.addWidget(ok_btn)
|
||||
action_row.addWidget(close_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()
|
||||
|
|
@ -13,6 +13,7 @@ from PySide6.QtWidgets import (
|
|||
QLineEdit,
|
||||
QSizePolicy,
|
||||
QStyle,
|
||||
QCompleter,
|
||||
)
|
||||
|
||||
from . import strings
|
||||
|
|
@ -25,7 +26,12 @@ class TagChip(QFrame):
|
|||
clicked = Signal(str) # tag name
|
||||
|
||||
def __init__(
|
||||
self, tag_id: int, name: str, color: str, parent: QWidget | None = None
|
||||
self,
|
||||
tag_id: int,
|
||||
name: str,
|
||||
color: str,
|
||||
parent: QWidget | None = None,
|
||||
show_remove: bool = True,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._id = tag_id
|
||||
|
|
@ -37,26 +43,26 @@ class TagChip(QFrame):
|
|||
self.setFrameShadow(QFrame.Raised)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(4, 0, 4, 0)
|
||||
layout.setContentsMargins(4, 2, 4, 2)
|
||||
layout.setSpacing(4)
|
||||
|
||||
color_lbl = QLabel()
|
||||
color_lbl.setFixedSize(10, 10)
|
||||
color_lbl.setStyleSheet(f"background-color: {color}; border-radius: 3px;")
|
||||
color_lbl.setStyleSheet(f"background-color: {color}; border-radius: 5px;")
|
||||
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))
|
||||
if show_remove:
|
||||
btn = QToolButton()
|
||||
btn.setText("×")
|
||||
btn.setAutoRaise(True)
|
||||
btn.clicked.connect(lambda: self.removeRequested.emit(self._id))
|
||||
layout.addWidget(btn)
|
||||
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
layout.addWidget(btn)
|
||||
|
||||
@property
|
||||
def tag_id(self) -> int:
|
||||
return self._id
|
||||
|
|
@ -70,8 +76,12 @@ class TagChip(QFrame):
|
|||
class PageTagsWidget(QFrame):
|
||||
"""
|
||||
Collapsible per-page tag editor shown in the left sidebar.
|
||||
Now displays tag chips even when collapsed.
|
||||
"""
|
||||
|
||||
tagActivated = Signal(str) # tag name
|
||||
tagAdded = Signal() # emitted when a tag is added to trigger autosave
|
||||
|
||||
def __init__(self, db: DBManager, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
|
|
@ -103,21 +113,25 @@ class PageTagsWidget(QFrame):
|
|||
header.addStretch(1)
|
||||
header.addWidget(self.manage_btn)
|
||||
|
||||
# Body (chips + add line)
|
||||
# Body (chips + add line - only visible when expanded)
|
||||
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 = FlowLayout(self.body, hspacing=4, vspacing=4)
|
||||
self.body_layout.addLayout(self.chip_row)
|
||||
# Chips container
|
||||
self.chip_container = QWidget()
|
||||
self.chip_layout = FlowLayout(self.chip_container, hspacing=4, vspacing=4)
|
||||
self.body_layout.addWidget(self.chip_container)
|
||||
|
||||
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)
|
||||
|
||||
# Setup autocomplete
|
||||
self._setup_autocomplete()
|
||||
|
||||
self.body_layout.addWidget(self.add_edit)
|
||||
self.body.setVisible(False)
|
||||
|
||||
main = QVBoxLayout(self)
|
||||
|
|
@ -129,23 +143,33 @@ class PageTagsWidget(QFrame):
|
|||
|
||||
def set_current_date(self, date_iso: str) -> None:
|
||||
self._current_date = date_iso
|
||||
# Only reload tags if expanded
|
||||
if self.toggle_btn.isChecked():
|
||||
self._reload_tags()
|
||||
else:
|
||||
# Keep it cheap while collapsed; reload only when expanded
|
||||
self._clear_chips()
|
||||
self._clear_chips() # Clear chips when collapsed
|
||||
self._setup_autocomplete() # Update autocomplete with all available tags
|
||||
|
||||
# ----- internals ---------------------------------------------------
|
||||
|
||||
def _setup_autocomplete(self) -> None:
|
||||
"""Setup autocomplete for the tag input with all existing tags"""
|
||||
all_tags = [name for _, name, _ in self._db.list_tags()]
|
||||
completer = QCompleter(all_tags, self.add_edit)
|
||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.add_edit.setCompleter(completer)
|
||||
|
||||
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()
|
||||
if checked:
|
||||
if self._current_date:
|
||||
self._reload_tags()
|
||||
self.add_edit.setFocus()
|
||||
|
||||
def _clear_chips(self) -> None:
|
||||
while self.chip_row.count():
|
||||
item = self.chip_row.takeAt(0)
|
||||
while self.chip_layout.count():
|
||||
item = self.chip_layout.takeAt(0)
|
||||
w = item.widget()
|
||||
if w is not None:
|
||||
w.deleteLater()
|
||||
|
|
@ -158,26 +182,56 @@ class PageTagsWidget(QFrame):
|
|||
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)
|
||||
# Always show remove button since chips only visible when expanded
|
||||
chip = TagChip(tag_id, name, color, self, show_remove=True)
|
||||
chip.removeRequested.connect(self._remove_tag)
|
||||
chip.clicked.connect(self._on_chip_clicked)
|
||||
self.chip_row.addWidget(chip)
|
||||
self.chip_layout.addWidget(chip)
|
||||
chip.show()
|
||||
chip.adjustSize()
|
||||
|
||||
# Force complete layout recalculation
|
||||
self.chip_layout.invalidate()
|
||||
self.chip_layout.activate()
|
||||
self.chip_container.updateGeometry()
|
||||
self.updateGeometry()
|
||||
# Process pending events to ensure layout is applied
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
def _on_add_tag(self) -> None:
|
||||
if not self._current_date:
|
||||
return
|
||||
|
||||
# If the completer popup is visible and user pressed Enter,
|
||||
# the completer will handle it - don't process it again
|
||||
if self.add_edit.completer() and self.add_edit.completer().popup().isVisible():
|
||||
return
|
||||
|
||||
new_tag = self.add_edit.text().strip()
|
||||
if not new_tag:
|
||||
return
|
||||
|
||||
# Combine current tags + new one, then write back
|
||||
# Get existing tags for current page
|
||||
existing = [
|
||||
name for _, name, _ in self._db.get_tags_for_page(self._current_date)
|
||||
]
|
||||
|
||||
# Check for duplicates (case-insensitive)
|
||||
if any(tag.lower() == new_tag.lower() for tag in existing):
|
||||
self.add_edit.clear()
|
||||
return
|
||||
|
||||
existing.append(new_tag)
|
||||
self._db.set_tags_for_page(self._current_date, existing)
|
||||
|
||||
self.add_edit.clear()
|
||||
self._reload_tags()
|
||||
self._setup_autocomplete() # Update autocomplete list
|
||||
|
||||
# Signal that a tag was added so main window can trigger autosave
|
||||
self.tagAdded.emit()
|
||||
|
||||
def _remove_tag(self, tag_id: int) -> None:
|
||||
if not self._current_date:
|
||||
|
|
@ -188,13 +242,15 @@ class PageTagsWidget(QFrame):
|
|||
self._reload_tags()
|
||||
|
||||
def _open_manager(self) -> None:
|
||||
from .tags_dialog import TagManagerDialog
|
||||
from .tag_browser import TagBrowserDialog
|
||||
|
||||
dlg = TagManagerDialog(self._db, self)
|
||||
dlg = TagBrowserDialog(self._db, self)
|
||||
dlg.openDateRequested.connect(lambda date_iso: self.tagActivated.emit(date_iso))
|
||||
if dlg.exec():
|
||||
# Names/colours may have changed
|
||||
# Reload tags after manager closes to pick up any changes
|
||||
if self._current_date:
|
||||
self._reload_tags()
|
||||
self._setup_autocomplete()
|
||||
|
||||
def _on_chip_clicked(self, name: str) -> None:
|
||||
self.tagActivated.emit(name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue