Compare commits
3 commits
6fb465c546
...
19593403b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 19593403b9 | |||
| 3713cc6c29 | |||
| 0e3ca64619 |
6 changed files with 126 additions and 22 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue