Compare commits
No commits in common. "81cf878ffdc9b5c38fb6d064bcdfb2f9b2121feb" and "943580091010c27cb0d122fc69fcf3781984a9e2" have entirely different histories.
81cf878ffd
...
9435800910
11 changed files with 67 additions and 108 deletions
|
|
@ -1,10 +1,3 @@
|
||||||
# 0.5.2
|
|
||||||
|
|
||||||
* Update icon again to remove background
|
|
||||||
* Adjust History icon and reorder toolbar items
|
|
||||||
* Try to address checkbox/bullet size issues (again)
|
|
||||||
* Fix HTML export of markdown (with newlines, tables and other styling preserved)
|
|
||||||
|
|
||||||
# 0.5.1
|
# 0.5.1
|
||||||
|
|
||||||
* Try to address Noto Sans font issue that works for both numbers and checkbox/bullets.
|
* Try to address Noto Sans font issue that works for both numbers and checkbox/bullets.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Bouquin ("Book-ahn") is a notebook and planner application written in Python, Qt and SQLCipher.
|
Bouquin ("Book-ahn") is a notebook and planner application written in Python, PyQt and SQLCipher.
|
||||||
|
|
||||||
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
|
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
|
||||||
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import datetime as _dt
|
||||||
import hashlib
|
import hashlib
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import markdown
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -441,33 +440,14 @@ class DBManager:
|
||||||
'<html lang="en">',
|
'<html lang="en">',
|
||||||
'<meta charset="utf-8">',
|
'<meta charset="utf-8">',
|
||||||
f"<title>{html.escape(title)}</title>",
|
f"<title>{html.escape(title)}</title>",
|
||||||
"<style>"
|
"<style>body{font:16px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;padding:24px;max-width:900px;margin:auto;}",
|
||||||
"body{font:16px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"
|
"article{padding:16px 0;border-bottom:1px solid #ddd;} time{font-weight:600;color:#333;} section{margin-top:8px;}</style>",
|
||||||
"padding:24px;max-width:900px;margin:auto;}"
|
|
||||||
"article{padding:16px 0;border-bottom:1px solid #ddd;}"
|
|
||||||
"article header time{font-weight:600;color:#333;}"
|
|
||||||
"section{margin-top:8px;}"
|
|
||||||
"table{border-collapse:collapse;margin-top:8px;}"
|
|
||||||
"th,td{border:1px solid #ddd;padding:4px 8px;text-align:left;}"
|
|
||||||
"</style>",
|
|
||||||
"<body>",
|
"<body>",
|
||||||
f"<h1>{html.escape(title)}</h1>",
|
f"<h1>{html.escape(title)}</h1>",
|
||||||
]
|
]
|
||||||
for d, c in entries:
|
for d, c in entries:
|
||||||
body_html = markdown.markdown(
|
|
||||||
c,
|
|
||||||
extensions=[
|
|
||||||
"extra",
|
|
||||||
"nl2br",
|
|
||||||
],
|
|
||||||
output_format="html5",
|
|
||||||
)
|
|
||||||
|
|
||||||
parts.append(
|
parts.append(
|
||||||
f"<article>"
|
f"<article><header><time>{html.escape(d)}</time></header><section>{c}</section></article>"
|
||||||
f"<header><time>{html.escape(d)}</time></header>"
|
|
||||||
f"<section>{body_html}</section>"
|
|
||||||
f"</article>"
|
|
||||||
)
|
)
|
||||||
parts.append("</body></html>")
|
parts.append("</body></html>")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,21 @@
|
||||||
viewBox="0 0 512 512"
|
viewBox="0 0 512 512"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
|
<!-- Rounded square background tile -->
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
rx="112"
|
||||||
|
ry="112"
|
||||||
|
fill="#0F172A"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Book cover -->
|
<!-- Book cover -->
|
||||||
<rect
|
<rect
|
||||||
x="116"
|
x="116"
|
||||||
y="76"
|
y="92"
|
||||||
width="280"
|
width="280"
|
||||||
height="360"
|
height="360"
|
||||||
rx="48"
|
rx="48"
|
||||||
|
|
@ -18,7 +29,7 @@
|
||||||
<!-- Book spine -->
|
<!-- Book spine -->
|
||||||
<rect
|
<rect
|
||||||
x="116"
|
x="116"
|
||||||
y="76"
|
y="92"
|
||||||
width="64"
|
width="64"
|
||||||
height="360"
|
height="360"
|
||||||
rx="40"
|
rx="40"
|
||||||
|
|
@ -28,14 +39,14 @@
|
||||||
|
|
||||||
<!-- Folded page corner (top-right triangle) -->
|
<!-- Folded page corner (top-right triangle) -->
|
||||||
<path
|
<path
|
||||||
d="M396 76 L356 76 L396 116 Z"
|
d="M396 92 L356 92 L396 132 Z"
|
||||||
fill="#FEF9C3"
|
fill="#FEF9C3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Keyhole: circular top -->
|
<!-- Keyhole: circular top -->
|
||||||
<circle
|
<circle
|
||||||
cx="256"
|
cx="256"
|
||||||
cy="256"
|
cy="260"
|
||||||
r="34"
|
r="34"
|
||||||
fill="#0F172A"
|
fill="#0F172A"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 887 B |
|
|
@ -119,22 +119,6 @@ class MarkdownEditor(QTextEdit):
|
||||||
self._apply_code_block_spacing()
|
self._apply_code_block_spacing()
|
||||||
QTimer.singleShot(0, self._update_code_block_row_backgrounds)
|
QTimer.singleShot(0, self._update_code_block_row_backgrounds)
|
||||||
|
|
||||||
def setFont(self, font: QFont) -> None: # type: ignore[override]
|
|
||||||
"""
|
|
||||||
Ensure that whenever the base editor font changes, our highlighter
|
|
||||||
re-computes checkbox / bullet formats.
|
|
||||||
"""
|
|
||||||
# Keep qfont in sync
|
|
||||||
self.qfont = QFont(font)
|
|
||||||
super().setFont(self.qfont)
|
|
||||||
|
|
||||||
# If the highlighter is already attached, let it rebuild its formats
|
|
||||||
highlighter = getattr(self, "highlighter", None)
|
|
||||||
if highlighter is not None:
|
|
||||||
refresh = getattr(highlighter, "refresh_for_font_change", None)
|
|
||||||
if callable(refresh):
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
def showEvent(self, e):
|
def showEvent(self, e):
|
||||||
super().showEvent(e)
|
super().showEvent(e)
|
||||||
# First time the widget is shown, Qt may rebuild layout once more.
|
# First time the widget is shown, Qt may rebuild layout once more.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from PySide6.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
QFontDatabase,
|
QFontDatabase,
|
||||||
QFontMetrics,
|
|
||||||
QGuiApplication,
|
QGuiApplication,
|
||||||
QPalette,
|
QPalette,
|
||||||
QSyntaxHighlighter,
|
QSyntaxHighlighter,
|
||||||
|
|
@ -34,14 +33,6 @@ class MarkdownHighlighter(QSyntaxHighlighter):
|
||||||
self._setup_formats()
|
self._setup_formats()
|
||||||
self.rehighlight()
|
self.rehighlight()
|
||||||
|
|
||||||
def refresh_for_font_change(self) -> None:
|
|
||||||
"""
|
|
||||||
Called when the editor's base font changes (zoom / settings).
|
|
||||||
It rebuilds any formats that depend on the editor font metrics.
|
|
||||||
"""
|
|
||||||
self._setup_formats()
|
|
||||||
self.rehighlight()
|
|
||||||
|
|
||||||
def _setup_formats(self):
|
def _setup_formats(self):
|
||||||
"""Setup text formats for different markdown elements."""
|
"""Setup text formats for different markdown elements."""
|
||||||
|
|
||||||
|
|
@ -119,21 +110,8 @@ class MarkdownHighlighter(QSyntaxHighlighter):
|
||||||
|
|
||||||
# Use Symbols font for checkbox and bullet glyphs if present
|
# Use Symbols font for checkbox and bullet glyphs if present
|
||||||
if self._editor is not None and hasattr(self._editor, "symbols_font_family"):
|
if self._editor is not None and hasattr(self._editor, "symbols_font_family"):
|
||||||
base_font = QFont(self._editor.qfont) # copy of editor font
|
base_size = self._editor.qfont.pointSize()
|
||||||
symbols_font = QFont(self._editor.symbols_font_family)
|
symbols_font = QFont(self._editor.symbols_font_family, base_size)
|
||||||
symbols_font.setPointSizeF(base_font.pointSizeF())
|
|
||||||
|
|
||||||
base_metrics = QFontMetrics(base_font)
|
|
||||||
sym_metrics = QFontMetrics(symbols_font)
|
|
||||||
|
|
||||||
# If Symbols glyphs are noticeably shorter than the text,
|
|
||||||
# scale them up so the visual heights roughly match.
|
|
||||||
if sym_metrics.height() > 0:
|
|
||||||
ratio = base_metrics.height() / sym_metrics.height()
|
|
||||||
if ratio > 1.05: # more than ~5% smaller
|
|
||||||
ratio = min(ratio, 1.4) # Oh, Tod, Tod. Don't overdo it.
|
|
||||||
symbols_font.setPointSizeF(symbols_font.pointSizeF() * ratio)
|
|
||||||
|
|
||||||
self.checkbox_format.setFont(symbols_font)
|
self.checkbox_format.setFont(symbols_font)
|
||||||
self.bullet_format.setFont(symbols_font)
|
self.bullet_format.setFont(symbols_font)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
self.toggle_btn.clicked.connect(self._on_toggle)
|
self.toggle_btn.clicked.connect(self._on_toggle)
|
||||||
|
|
||||||
self.add_btn = QToolButton()
|
self.add_btn = QToolButton()
|
||||||
self.add_btn.setText("⏰")
|
self.add_btn.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
|
||||||
self.add_btn.setToolTip("Add Reminder")
|
self.add_btn.setToolTip("Add Reminder")
|
||||||
self.add_btn.setAutoRaise(True)
|
self.add_btn.setAutoRaise(True)
|
||||||
self.add_btn.clicked.connect(self._add_reminder)
|
self.add_btn.clicked.connect(self._add_reminder)
|
||||||
|
|
|
||||||
|
|
@ -100,17 +100,6 @@ class ToolBar(QToolBar):
|
||||||
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
|
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
|
||||||
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
||||||
|
|
||||||
# Images
|
|
||||||
self.actInsertImg = QAction("📸", self)
|
|
||||||
self.actInsertImg.setToolTip(strings._("insert_images"))
|
|
||||||
self.actInsertImg.setShortcut("Ctrl+Shift+I")
|
|
||||||
self.actInsertImg.triggered.connect(self.insertImageRequested)
|
|
||||||
|
|
||||||
# History button
|
|
||||||
self.actHistory = QAction("🔁", self)
|
|
||||||
self.actHistory.setToolTip(strings._("history"))
|
|
||||||
self.actHistory.triggered.connect(self.historyRequested)
|
|
||||||
|
|
||||||
# Alarm / reminder
|
# Alarm / reminder
|
||||||
self.actAlarm = QAction("⏰", self)
|
self.actAlarm = QAction("⏰", self)
|
||||||
self.actAlarm.setToolTip(strings._("toolbar_alarm"))
|
self.actAlarm.setToolTip(strings._("toolbar_alarm"))
|
||||||
|
|
@ -126,6 +115,17 @@ class ToolBar(QToolBar):
|
||||||
self.actTable.setToolTip(strings._("toolbar_insert_table"))
|
self.actTable.setToolTip(strings._("toolbar_insert_table"))
|
||||||
self.actTable.triggered.connect(self.tableRequested)
|
self.actTable.triggered.connect(self.tableRequested)
|
||||||
|
|
||||||
|
# Images
|
||||||
|
self.actInsertImg = QAction("📸", self)
|
||||||
|
self.actInsertImg.setToolTip(strings._("insert_images"))
|
||||||
|
self.actInsertImg.setShortcut("Ctrl+Shift+I")
|
||||||
|
self.actInsertImg.triggered.connect(self.insertImageRequested)
|
||||||
|
|
||||||
|
# History button
|
||||||
|
self.actHistory = QAction("⎌", self)
|
||||||
|
self.actHistory.setToolTip(strings._("history"))
|
||||||
|
self.actHistory.triggered.connect(self.historyRequested)
|
||||||
|
|
||||||
# Set exclusive buttons in QActionGroups
|
# Set exclusive buttons in QActionGroups
|
||||||
self.grpHeadings = QActionGroup(self)
|
self.grpHeadings = QActionGroup(self)
|
||||||
self.grpHeadings.setExclusive(True)
|
self.grpHeadings.setExclusive(True)
|
||||||
|
|
@ -162,10 +162,10 @@ class ToolBar(QToolBar):
|
||||||
self.actBullets,
|
self.actBullets,
|
||||||
self.actNumbers,
|
self.actNumbers,
|
||||||
self.actCheckboxes,
|
self.actCheckboxes,
|
||||||
self.actTable,
|
|
||||||
self.actInsertImg,
|
|
||||||
self.actAlarm,
|
self.actAlarm,
|
||||||
self.actTimer,
|
self.actTimer,
|
||||||
|
self.actTable,
|
||||||
|
self.actInsertImg,
|
||||||
self.actHistory,
|
self.actHistory,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
@ -195,7 +195,7 @@ class ToolBar(QToolBar):
|
||||||
self._style_letter_button(self.actTable, "⊞")
|
self._style_letter_button(self.actTable, "⊞")
|
||||||
|
|
||||||
# History
|
# History
|
||||||
self._style_letter_button(self.actHistory, "🔁")
|
self._style_letter_button(self.actHistory, "⎌")
|
||||||
|
|
||||||
def _style_letter_button(
|
def _style_letter_button(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
17
poetry.lock
generated
17
poetry.lock
generated
|
|
@ -307,21 +307,6 @@ files = [
|
||||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markdown"
|
|
||||||
version = "3.10"
|
|
||||||
description = "Python implementation of John Gruber's Markdown."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.10"
|
|
||||||
files = [
|
|
||||||
{file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"},
|
|
||||||
{file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
|
|
||||||
testing = ["coverage", "pyyaml"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "25.0"
|
version = "25.0"
|
||||||
|
|
@ -759,4 +744,4 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.14"
|
python-versions = ">=3.10,<3.14"
|
||||||
content-hash = "86db2b4d6498ce9eba7fa9aedf9ce58fec6a63542b5f3bdb3cf6c4067e5b87df"
|
content-hash = "d5fd8ea759b6bd3f23336930bdce9241659256ed918ec31746787cc86e817235"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bouquin"
|
name = "bouquin"
|
||||||
version = "0.5.2"
|
version = "0.5.1"
|
||||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
@ -14,7 +14,6 @@ python = ">=3.10,<3.14"
|
||||||
pyside6 = ">=6.8.1,<7.0.0"
|
pyside6 = ">=6.8.1,<7.0.0"
|
||||||
sqlcipher3-wheels = "^0.5.5.post0"
|
sqlcipher3-wheels = "^0.5.5.post0"
|
||||||
requests = "^2.32.5"
|
requests = "^2.32.5"
|
||||||
markdown = "^3.10"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
bouquin = "bouquin.__main__:main"
|
bouquin = "bouquin.__main__:main"
|
||||||
|
|
|
||||||
|
|
@ -765,6 +765,35 @@ def test_reminder_not_today_skipped(qtbot, fresh_db):
|
||||||
assert len(triggered_texts) == 0
|
assert len(triggered_texts) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_reminder_context_menu_single_item(qtbot, fresh_db):
|
||||||
|
"""Test context menu for a single reminder item."""
|
||||||
|
reminder = Reminder(
|
||||||
|
id=None,
|
||||||
|
text="Test reminder",
|
||||||
|
reminder_type=ReminderType.ONCE,
|
||||||
|
time_str="14:30",
|
||||||
|
date_iso=date.today().isoformat(),
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
fresh_db.save_reminder(reminder)
|
||||||
|
|
||||||
|
reminders_widget = UpcomingRemindersWidget(fresh_db)
|
||||||
|
qtbot.addWidget(reminders_widget)
|
||||||
|
reminders_widget.show()
|
||||||
|
|
||||||
|
# Refresh to populate the list
|
||||||
|
reminders_widget.refresh()
|
||||||
|
|
||||||
|
# Select the first item
|
||||||
|
if reminders_widget.reminder_list.count() > 0:
|
||||||
|
reminders_widget.reminder_list.setCurrentRow(0)
|
||||||
|
|
||||||
|
# Show context menu (won't actually display in tests)
|
||||||
|
reminders_widget._show_reminder_context_menu(
|
||||||
|
reminders_widget.reminder_list.pos()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_reminder_context_menu_no_selection(qtbot, fresh_db):
|
def test_reminder_context_menu_no_selection(qtbot, fresh_db):
|
||||||
"""Test context menu with no selection returns early."""
|
"""Test context menu with no selection returns early."""
|
||||||
reminders_widget = UpcomingRemindersWidget(fresh_db)
|
reminders_widget = UpcomingRemindersWidget(fresh_db)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue