WIP
This commit is contained in:
parent
df7ae0b42d
commit
5e283ecf17
8 changed files with 294 additions and 21 deletions
|
|
@ -25,6 +25,7 @@ _TAG_COLORS = [
|
|||
"#E0BAFF", # soft purple
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DBConfig:
|
||||
path: Path
|
||||
|
|
@ -198,7 +199,6 @@ class DBManager:
|
|||
).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.
|
||||
|
|
@ -560,6 +560,28 @@ class DBManager:
|
|||
cur.execute("DELETE FROM page_tags WHERE tag_id=?;", (tag_id,))
|
||||
cur.execute("DELETE FROM tags WHERE id=?;", (tag_id,))
|
||||
|
||||
def get_pages_for_tag(self, tag_name: str) -> list[Entry]:
|
||||
"""
|
||||
Return (date, content) for pages that have the given tag.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT p.date, v.content
|
||||
FROM pages AS p
|
||||
JOIN versions AS v
|
||||
ON v.id = p.current_version_id
|
||||
JOIN page_tags pt
|
||||
ON pt.page_date = p.date
|
||||
JOIN tags t
|
||||
ON t.id = pt.tag_id
|
||||
WHERE LOWER(t.name) = LOWER(?)
|
||||
ORDER BY p.date DESC;
|
||||
""",
|
||||
(tag_name,),
|
||||
).fetchall()
|
||||
return [(r[0], r[1]) for r in rows]
|
||||
|
||||
def close(self) -> None:
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
|
|
|
|||
88
bouquin/flow_layout.py
Normal file
88
bouquin/flow_layout.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QPoint, QRect, QSize, Qt
|
||||
from PySide6.QtWidgets import QLayout, QSizePolicy
|
||||
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
def __init__(
|
||||
self, parent=None, margin: int = 0, hspacing: int = 4, vspacing: int = 4
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._items = []
|
||||
self._hspace = hspacing
|
||||
self._vspace = vspacing
|
||||
self.setContentsMargins(margin, margin, margin, margin)
|
||||
|
||||
def addItem(self, item):
|
||||
self._items.append(item)
|
||||
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._items):
|
||||
return self._items[index]
|
||||
return None
|
||||
|
||||
def takeAt(self, index):
|
||||
if 0 <= index < len(self._items):
|
||||
return self._items.pop(index)
|
||||
return None
|
||||
|
||||
def count(self):
|
||||
return len(self._items)
|
||||
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientations(Qt.Orientation(0))
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width: int) -> int:
|
||||
return self._do_layout(QRect(0, 0, width, 0), test_only=True)
|
||||
|
||||
def setGeometry(self, rect: QRect):
|
||||
super().setGeometry(rect)
|
||||
self._do_layout(rect, test_only=False)
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self) -> QSize:
|
||||
size = QSize()
|
||||
for item in self._items:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
left, top, right, bottom = self.getContentsMargins()
|
||||
size += QSize(left + right, top + bottom)
|
||||
return size
|
||||
|
||||
def _do_layout(self, rect: QRect, test_only: bool) -> int:
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
line_height = 0
|
||||
|
||||
left, top, right, bottom = self.getContentsMargins()
|
||||
effective_rect = rect.adjusted(+left, +top, -right, -bottom)
|
||||
x = effective_rect.x()
|
||||
y = effective_rect.y()
|
||||
max_right = effective_rect.right()
|
||||
|
||||
for item in self._items:
|
||||
wid = item.widget()
|
||||
if wid is None or not wid.isVisible():
|
||||
continue
|
||||
space_x = self._hspace
|
||||
space_y = self._vspace
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
if next_x - space_x > max_right and line_height > 0:
|
||||
# Wrap
|
||||
x = effective_rect.x()
|
||||
y = y + line_height + space_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
line_height = 0
|
||||
|
||||
if not test_only:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
|
||||
x = next_x
|
||||
line_height = max(line_height, item.sizeHint().height())
|
||||
|
||||
return y + line_height - rect.y() + bottom
|
||||
|
|
@ -112,11 +112,15 @@
|
|||
"toolbar_heading": "Heading",
|
||||
"toolbar_toggle_checkboxes": "Toggle checkboxes",
|
||||
"tags": "Tags",
|
||||
"manage_tags": "Manage tags…",
|
||||
"add_tag_placeholder": "Add tag and press Enter…",
|
||||
"manage_tags": "Manage tags on this page",
|
||||
"add_tag_placeholder": "Add a tag and press Enter",
|
||||
"tag_browser_title": "Tag Browser",
|
||||
"tag_name": "Tag name",
|
||||
"tag_color_hex": "Hex colour",
|
||||
"pick_color": "Pick colour…",
|
||||
"pick_color": "Pick colour",
|
||||
"invalid_color_title": "Invalid colour",
|
||||
"invalid_color_message": "Please enter a valid hex colour like #RRGGBB."
|
||||
"invalid_color_message": "Please enter a valid hex colour like #RRGGBB.",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"ok": "OK"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ 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
|
||||
|
||||
|
|
@ -180,6 +181,11 @@ 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)
|
||||
|
||||
|
|
@ -502,11 +508,8 @@ 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)
|
||||
self._update_tag_views_for_date(date_iso)
|
||||
|
||||
# Reconnect toolbar to new active editor
|
||||
self._sync_toolbar()
|
||||
|
|
@ -639,8 +642,7 @@ class MainWindow(QMainWindow):
|
|||
self._reorder_tabs_by_date()
|
||||
|
||||
# sync tags
|
||||
if hasattr(self, "tags"):
|
||||
self.tags.set_current_date(date_iso)
|
||||
self._update_tag_views_for_date(date_iso)
|
||||
|
||||
def _load_date_into_editor(self, date: QDate, extra_data=False):
|
||||
"""Load a specific date's content into a given editor."""
|
||||
|
|
@ -1005,6 +1007,20 @@ class MainWindow(QMainWindow):
|
|||
for path_str in paths:
|
||||
self.editor.insert_image_from_path(Path(path_str))
|
||||
|
||||
# ----------- Tags handler ----------------#
|
||||
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
|
||||
|
||||
dlg = TagBrowserDialog(self.db, self, focus_tag=tag_name)
|
||||
dlg.openDateRequested.connect(self._load_selected_date)
|
||||
dlg.exec()
|
||||
|
||||
# ----------- Settings handler ------------#
|
||||
def _open_settings(self):
|
||||
dlg = SettingsDialog(self.cfg, self.db, self)
|
||||
|
|
|
|||
63
bouquin/status_tags_widget.py
Normal file
63
bouquin/status_tags_widget.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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)
|
||||
60
bouquin/tag_browser.py
Normal file
60
bouquin/tag_browser.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# tag_browser.py
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
from .db import DBManager
|
||||
from . import strings
|
||||
|
||||
|
||||
class TagBrowserDialog(QDialog):
|
||||
openDateRequested = Signal(str)
|
||||
|
||||
def __init__(self, db: DBManager, parent=None, focus_tag: str | None = None):
|
||||
super().__init__(parent)
|
||||
self._db = db
|
||||
self.setWindowTitle(strings._("tag_browser_title"))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabels([strings._("tag"), strings._("date")])
|
||||
self.tree.itemActivated.connect(self._on_item_activated)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
close_btn = QPushButton(strings._("close"))
|
||||
close_btn.clicked.connect(self.accept)
|
||||
layout.addWidget(close_btn)
|
||||
|
||||
self._populate(focus_tag)
|
||||
|
||||
def _populate(self, focus_tag: str | None):
|
||||
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)
|
||||
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)
|
||||
root.addChild(child)
|
||||
|
||||
if focus_tag and name.lower() == focus_tag.lower():
|
||||
focus_item = root
|
||||
|
||||
if focus_item:
|
||||
self.tree.expandItem(focus_item)
|
||||
self.tree.setCurrentItem(focus_item)
|
||||
|
||||
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)
|
||||
|
|
@ -15,6 +15,7 @@ from PySide6.QtWidgets import (
|
|||
from . import strings
|
||||
from .db import DBManager
|
||||
|
||||
|
||||
class TagManagerDialog(QDialog):
|
||||
def __init__(self, db: DBManager, parent=None):
|
||||
super().__init__(parent)
|
||||
|
|
@ -42,12 +43,12 @@ class TagManagerDialog(QDialog):
|
|||
|
||||
action_row = QHBoxLayout()
|
||||
ok_btn = QPushButton(strings._("ok"))
|
||||
cancel_btn = QPushButton(strings._("cancel"))
|
||||
close_btn = QPushButton(strings._("close"))
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
close_btn.clicked.connect(self.reject)
|
||||
action_row.addStretch(1)
|
||||
action_row.addWidget(ok_btn)
|
||||
action_row.addWidget(cancel_btn)
|
||||
action_row.addWidget(close_btn)
|
||||
layout.addLayout(action_row)
|
||||
|
||||
self.add_btn.clicked.connect(self._add_row)
|
||||
|
|
|
|||
|
|
@ -17,13 +17,20 @@ from PySide6.QtWidgets import (
|
|||
|
||||
from . import strings
|
||||
from .db import DBManager
|
||||
from .flow_layout import FlowLayout
|
||||
|
||||
|
||||
class TagChip(QFrame):
|
||||
removeRequested = Signal(int) # tag_id
|
||||
clicked = Signal(str) # tag name
|
||||
|
||||
def __init__(self, tag_id: int, name: str, color: str, parent: QWidget | None = None):
|
||||
def __init__(
|
||||
self, tag_id: int, name: str, color: str, parent: QWidget | None = None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._id = tag_id
|
||||
self._name = name
|
||||
|
||||
self.setObjectName("TagChip")
|
||||
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
|
|
@ -45,12 +52,20 @@ class TagChip(QFrame):
|
|||
btn.setText("×")
|
||||
btn.setAutoRaise(True)
|
||||
btn.clicked.connect(lambda: self.removeRequested.emit(self._id))
|
||||
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
layout.addWidget(btn)
|
||||
|
||||
@property
|
||||
def tag_id(self) -> int:
|
||||
return self._id
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
if ev.button() == Qt.LeftButton:
|
||||
self.clicked.emit(self._name)
|
||||
super().mouseReleaseEvent(ev)
|
||||
|
||||
|
||||
class PageTagsWidget(QFrame):
|
||||
"""
|
||||
|
|
@ -75,7 +90,9 @@ class PageTagsWidget(QFrame):
|
|||
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.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)
|
||||
|
|
@ -93,9 +110,7 @@ class PageTagsWidget(QFrame):
|
|||
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.chip_row = FlowLayout(self.body, hspacing=4, vspacing=4)
|
||||
self.body_layout.addLayout(self.chip_row)
|
||||
|
||||
self.add_edit = QLineEdit()
|
||||
|
|
@ -145,8 +160,8 @@ class PageTagsWidget(QFrame):
|
|||
for tag_id, name, color in tags:
|
||||
chip = TagChip(tag_id, name, color, self)
|
||||
chip.removeRequested.connect(self._remove_tag)
|
||||
chip.clicked.connect(self._on_chip_clicked)
|
||||
self.chip_row.addWidget(chip)
|
||||
self.chip_row.addStretch(1)
|
||||
|
||||
def _on_add_tag(self) -> None:
|
||||
if not self._current_date:
|
||||
|
|
@ -156,7 +171,9 @@ class PageTagsWidget(QFrame):
|
|||
return
|
||||
|
||||
# Combine current tags + new one, then write back
|
||||
existing = [name for _, name, _ in self._db.get_tags_for_page(self._current_date)]
|
||||
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()
|
||||
|
|
@ -179,3 +196,5 @@ class PageTagsWidget(QFrame):
|
|||
if self._current_date:
|
||||
self._reload_tags()
|
||||
|
||||
def _on_chip_clicked(self, name: str) -> None:
|
||||
self.tagActivated.emit(name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue