Compare commits

...

3 commits

6 changed files with 126 additions and 22 deletions

View file

@ -1,3 +1,8 @@
# 0.1.10
* Improve search results window and highlight in calendar when there are matches.
* Fix styling issue with text that comes after a URL, so it doesn't appear as part of the URL.
# 0.1.9 # 0.1.9
* More styling/toolbar fixes to support toggled-on styles, exclusive styles (e.g it should not be * More styling/toolbar fixes to support toggled-on styles, exclusive styles (e.g it should not be

View file

@ -349,7 +349,7 @@ class Editor(QTextEdit):
if source.hasImage(): if source.hasImage():
img = self._to_qimage(source.imageData()) img = self._to_qimage(source.imageData())
if img is not None: if img is not None:
self._insert_qimage_at_cursor(self, img, autoscale=True) self._insert_qimage_at_cursor(img, autoscale=True)
return return
# 2) File URLs (drag/drop or paste) # 2) File URLs (drag/drop or paste)
@ -496,12 +496,21 @@ class Editor(QTextEdit):
cur_fmt = self.textCursor().charFormat() cur_fmt = self.textCursor().charFormat()
# Do nothing unless either side indicates we're in/propagating an anchor # Do nothing unless either side indicates we're in/propagating an anchor
if not (ins_fmt.isAnchor() or cur_fmt.isAnchor()): if not (
ins_fmt.isAnchor()
or cur_fmt.isAnchor()
or ins_fmt.fontUnderline()
or ins_fmt.foreground().style() != Qt.NoBrush
):
return return
nf = QTextCharFormat(ins_fmt) nf = QTextCharFormat(ins_fmt)
# stop the link itself
nf.setAnchor(False) nf.setAnchor(False)
nf.setAnchorHref("") nf.setAnchorHref("")
# also stop the link *styling*
nf.setFontUnderline(False)
nf.clearForeground()
self.setCurrentCharFormat(nf) self.setCurrentCharFormat(nf)

View file

@ -165,12 +165,10 @@ class HistoryDialog(QDialog):
sel_id = item.data(Qt.UserRole) sel_id = item.data(Qt.UserRole)
if sel_id == self._current_id: if sel_id == self._current_id:
return return
sel = self._db.get_version(version_id=sel_id)
vno = sel["version_no"]
# Flip head pointer # Flip head pointer
try: try:
self._db.revert_to_version(self._date, version_id=sel_id) self._db.revert_to_version(self._date, version_id=sel_id)
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Revert failed", str(e)) QMessageBox.critical(self, "Revert failed", str(e))
return return
self.accept() # let the caller refresh the editor self.accept()

View file

@ -17,11 +17,12 @@ from PySide6.QtCore import (
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QAction, QAction,
QBrush,
QColor,
QCursor, QCursor,
QDesktopServices, QDesktopServices,
QFont, QFont,
QGuiApplication, QGuiApplication,
QTextCharFormat,
QTextListFormat, QTextListFormat,
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@ -132,15 +133,15 @@ class MainWindow(QMainWindow):
self.search = Search(self.db) self.search = Search(self.db)
self.search.openDateRequested.connect(self._load_selected_date) self.search.openDateRequested.connect(self._load_selected_date)
self.search.resultDatesChanged.connect(self._on_search_dates_changed)
# Lock the calendar to the left panel at the top to stop it stretching # Lock the calendar to the left panel at the top to stop it stretching
# when the main window is resized. # when the main window is resized.
left_panel = QWidget() left_panel = QWidget()
left_layout = QVBoxLayout(left_panel) left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(8, 8, 8, 8) left_layout.setContentsMargins(8, 8, 8, 8)
left_layout.addWidget(self.calendar, alignment=Qt.AlignTop) left_layout.addWidget(self.calendar)
left_layout.addWidget(self.search, alignment=Qt.AlignBottom) left_layout.addWidget(self.search)
left_layout.addStretch(1)
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16) left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
# This is the note-taking editor # This is the note-taking editor
@ -313,22 +314,44 @@ class MainWindow(QMainWindow):
if self._try_connect(): if self._try_connect():
return True return True
def _on_search_dates_changed(self, date_strs: list[str]):
dates = set()
for ds in date_strs or []:
qd = QDate.fromString(ds, "yyyy-MM-dd")
if qd.isValid():
dates.add(qd)
self._apply_search_highlights(dates)
def _apply_search_highlights(self, dates: set):
yellow = QBrush(QColor("#fff9c4"))
old = getattr(self, "_search_highlighted_dates", set())
for d in old - dates: # clear removed
fmt = self.calendar.dateTextFormat(d)
fmt.setBackground(Qt.transparent)
self.calendar.setDateTextFormat(d, fmt)
for d in dates: # apply new/current
fmt = self.calendar.dateTextFormat(d)
fmt.setBackground(yellow)
self.calendar.setDateTextFormat(d, fmt)
self._search_highlighted_dates = dates
def _refresh_calendar_marks(self): def _refresh_calendar_marks(self):
""" """Make days with entries bold, but keep any search highlight backgrounds."""
Sets a bold marker on the day to indicate that text exists
for that day.
"""
fmt_bold = QTextCharFormat()
fmt_bold.setFontWeight(QFont.Weight.Bold)
# Clear previous marks
for d in getattr(self, "_marked_dates", set()): for d in getattr(self, "_marked_dates", set()):
self.calendar.setDateTextFormat(d, QTextCharFormat()) fmt = self.calendar.dateTextFormat(d)
fmt.setFontWeight(QFont.Weight.Normal) # remove bold only
self.calendar.setDateTextFormat(d, fmt)
self._marked_dates = set() self._marked_dates = set()
try: try:
for date_iso in self.db.dates_with_content(): for date_iso in self.db.dates_with_content():
qd = QDate.fromString(date_iso, "yyyy-MM-dd") qd = QDate.fromString(date_iso, "yyyy-MM-dd")
if qd.isValid(): if qd.isValid():
self.calendar.setDateTextFormat(qd, fmt_bold) fmt = self.calendar.dateTextFormat(qd)
fmt.setFontWeight(QFont.Weight.Bold) # add bold only
self.calendar.setDateTextFormat(qd, fmt)
self._marked_dates.add(qd) self._marked_dates.add(qd)
except Exception: except Exception:
pass pass

View file

@ -6,10 +6,12 @@ from typing import Iterable, Tuple
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QTextCharFormat, QTextCursor, QTextDocument from PySide6.QtGui import QFont, QTextCharFormat, QTextCursor, QTextDocument
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame,
QLabel, QLabel,
QLineEdit, QLineEdit,
QListWidget, QListWidget,
QListWidgetItem, QListWidgetItem,
QSizePolicy,
QHBoxLayout, QHBoxLayout,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@ -23,6 +25,7 @@ class Search(QWidget):
"""Encapsulates the search UI + logic and emits a signal when a result is chosen.""" """Encapsulates the search UI + logic and emits a signal when a result is chosen."""
openDateRequested = Signal(str) openDateRequested = Signal(str)
resultDatesChanged = Signal(list)
def __init__(self, db, parent: QWidget | None = None): def __init__(self, db, parent: QWidget | None = None):
super().__init__(parent) super().__init__(parent)
@ -30,17 +33,21 @@ class Search(QWidget):
self.search = QLineEdit() self.search = QLineEdit()
self.search.setPlaceholderText("Search for notes here") self.search.setPlaceholderText("Search for notes here")
self.search.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.search.textChanged.connect(self._search) self.search.textChanged.connect(self._search)
self.results = QListWidget() self.results = QListWidget()
self.results.setUniformItemSizes(False) self.results.setUniformItemSizes(False)
self.results.setSelectionMode(self.results.SelectionMode.SingleSelection) self.results.setSelectionMode(self.results.SelectionMode.SingleSelection)
self.results.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
self.results.itemClicked.connect(self._open_selected) self.results.itemClicked.connect(self._open_selected)
self.results.hide() self.results.hide()
self.results.setMinimumHeight(250)
lay = QVBoxLayout(self) lay = QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0) lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(6) lay.setSpacing(6)
lay.setAlignment(Qt.AlignTop)
lay.addWidget(self.search) lay.addWidget(self.search)
lay.addWidget(self.results) lay.addWidget(self.results)
@ -58,6 +65,7 @@ class Search(QWidget):
if not q: if not q:
self.results.clear() self.results.clear()
self.results.hide() self.results.hide()
self.resultDatesChanged.emit([]) # clear highlights
return return
try: try:
@ -73,8 +81,10 @@ class Search(QWidget):
rows = list(rows) rows = list(rows)
if not rows: if not rows:
self.results.hide() self.results.hide()
self.resultDatesChanged.emit([]) # clear highlights
return return
self.resultDatesChanged.emit(sorted({d for d, _ in rows}))
self.results.show() self.results.show()
for date_str, content in rows: for date_str, content in rows:
@ -90,12 +100,13 @@ class Search(QWidget):
outer.setSpacing(2) outer.setSpacing(2)
# Date label (plain text) # Date label (plain text)
date_lbl = QLabel(date_str) date_lbl = QLabel()
date_lbl.setTextFormat(Qt.TextFormat.PlainText) date_lbl.setTextFormat(Qt.TextFormat.RichText)
date_lbl.setText(f"<i>{date_str}</i>:")
date_f = date_lbl.font() date_f = date_lbl.font()
date_f.setPointSizeF(date_f.pointSizeF() - 1) date_f.setPointSizeF(date_f.pointSizeF() + 1)
date_lbl.setFont(date_f) date_lbl.setFont(date_f)
date_lbl.setStyleSheet("color:#666;") date_lbl.setStyleSheet("color:#000;")
outer.addWidget(date_lbl) outer.addWidget(date_lbl)
# Preview row with optional ellipses # Preview row with optional ellipses
@ -127,6 +138,11 @@ class Search(QWidget):
outer.addWidget(row) outer.addWidget(row)
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
outer.addWidget(line)
# ---- Add to list ---- # ---- Add to list ----
item = QListWidgetItem() item = QListWidgetItem()
item.setData(Qt.ItemDataRole.UserRole, date_str) item.setData(Qt.ItemDataRole.UserRole, date_str)

View file

@ -4,6 +4,8 @@ from PySide6.QtTest import QTest
from bouquin.editor import Editor from bouquin.editor import Editor
import re
def _move_cursor_to_first_image(editor: Editor) -> QTextImageFormat | None: def _move_cursor_to_first_image(editor: Editor) -> QTextImageFormat | None:
c = editor.textCursor() c = editor.textCursor()
@ -21,6 +23,57 @@ def _move_cursor_to_first_image(editor: Editor) -> QTextImageFormat | None:
return None return None
def _fmt_at(editor: Editor, pos: int):
c = editor.textCursor()
c.setPosition(pos)
c.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 1)
return c.charFormat()
def test_space_breaks_link_anchor_and_styling(qtbot):
e = Editor()
e.resize(600, 300)
e.show()
qtbot.waitExposed(e)
# Type a URL, which should be linkified (anchor + underline + blue)
url = "https://mig5.net"
QTest.keyClicks(e, url)
qtbot.waitUntil(lambda: e.toPlainText() == url)
# Sanity: characters within the URL are anchors
for i in range(len(url)):
assert _fmt_at(e, i).isAnchor()
# Hit Space Editor.keyPressEvent() should call _break_anchor_for_next_char()
QTest.keyClick(e, Qt.Key_Space)
# Type some normal text; it must not inherit the link formatting
tail = "this is a test"
QTest.keyClicks(e, tail)
qtbot.waitUntil(lambda: e.toPlainText().endswith(tail))
txt = e.toPlainText()
# Find where our 'tail' starts
start = txt.index(tail)
end = start + len(tail)
# None of the trailing characters should be part of an anchor or visually underlined
for i in range(start, end):
fmt = _fmt_at(e, i)
assert not fmt.isAnchor(), f"char {i} unexpectedly still has an anchor"
assert not fmt.fontUnderline(), f"char {i} unexpectedly still underlined"
# Optional: ensure the HTML only wraps the URL in <a>, not the trailing text
html = e.document().toHtml()
assert re.search(
r'<a [^>]*href="https?://mig5\.net"[^>]*>(?:<span[^>]*>)?https?://mig5\.net(?:</span>)?</a>\s+this is a test',
html,
re.S,
), html
assert "this is a test</a>" not in html
def test_embed_qimage_saved_as_data_url(qtbot): def test_embed_qimage_saved_as_data_url(qtbot):
e = Editor() e = Editor()
e.resize(600, 400) e.resize(600, 400)