Compare commits

..

2 commits

3 changed files with 143 additions and 31 deletions

View file

@ -1,3 +1,8 @@
# 0.2.1.3
* Ensure checkbox only can get checked on/off if it is clicked right on its block position, not any click on the whole line
* Fix code backticks to not show but still be able to type code easily
# 0.2.1.2
* Ensure tabs are ordered by calendar date

View file

@ -8,6 +8,7 @@ from PySide6.QtGui import (
QColor,
QFont,
QFontDatabase,
QFontMetrics,
QImage,
QPalette,
QGuiApplication,
@ -17,7 +18,7 @@ from PySide6.QtGui import (
QSyntaxHighlighter,
QTextImageFormat,
)
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, QRect
from PySide6.QtWidgets import QTextEdit
from .theme import ThemeManager, Theme
@ -33,6 +34,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
# Recompute formats whenever the app theme changes
try:
self.theme_manager.themeChanged.connect(self._on_theme_changed)
self.textChanged.connect(self._refresh_codeblock_margins)
except Exception:
pass
@ -64,6 +66,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.code_block_format = QTextCharFormat()
self.code_block_format.setFont(mono)
self.code_block_format.setFontFixedPitch(True)
pal = QGuiApplication.palette()
if self.theme_manager.current() == Theme.DARK:
# In dark mode, use a darker panel-like background
@ -96,6 +99,36 @@ class MarkdownHighlighter(QSyntaxHighlighter):
# Also make them very faint in case they still show
self.syntax_format.setForeground(QColor(250, 250, 250))
def _refresh_codeblock_margins(self):
"""Give code blocks a small left/right margin to separate them visually."""
doc = self.document()
block = doc.begin()
in_code = False
while block.isValid():
txt = block.text().strip()
cursor = QTextCursor(block)
fmt = block.blockFormat()
if txt.startswith("```"):
# fence lines: small vertical spacing, same left indent
need = (12, 6, 6) # left, top, bottom (px-like)
if (fmt.leftMargin(), fmt.topMargin(), fmt.bottomMargin()) != need:
fmt.setLeftMargin(12)
fmt.setRightMargin(6)
fmt.setTopMargin(6)
fmt.setBottomMargin(6)
cursor.setBlockFormat(fmt)
in_code = not in_code
elif in_code:
# inside the code block
if fmt.leftMargin() != 12 or fmt.rightMargin() != 6:
fmt.setLeftMargin(12)
fmt.setRightMargin(6)
cursor.setBlockFormat(fmt)
block = block.next()
def highlightBlock(self, text: str):
"""Apply formatting to a block of text based on markdown syntax."""
if not text:
@ -107,12 +140,17 @@ class MarkdownHighlighter(QSyntaxHighlighter):
# Check for code block fences
if text.strip().startswith("```"):
# Toggle code block state
# background for the whole fence line (so block looks continuous)
self.setFormat(0, len(text), self.code_block_format)
# hide the three backticks themselves
idx = text.find("```")
if idx != -1:
self.setFormat(idx, 3, self.syntax_format)
# toggle code-block state and stop; next line picks up state
in_code_block = not in_code_block
self.setCurrentBlockState(1 if in_code_block else 0)
# Format the fence markers - but keep them somewhat visible for editing
# Use code format instead of syntax format so cursor is visible
self.setFormat(0, len(text), self.code_format)
return
if in_code_block:
@ -447,6 +485,36 @@ class MarkdownEditor(QTextEdit):
def keyPressEvent(self, event):
"""Handle special key events for markdown editing."""
# --- Auto-close code fences when typing the 3rd backtick at line start ---
if event.text() == "`":
c = self.textCursor()
block = c.block()
line = block.text()
pos_in_block = c.position() - block.position()
# text before caret on this line
before = line[:pos_in_block]
# If we've typed exactly two backticks at line start (or after whitespace),
# treat this backtick as the "third" and expand to a full fenced block.
if before.endswith("``") and before.strip() == "``":
start = (
block.position() + pos_in_block - 2
) # start of the two backticks
edit = QTextCursor(self.document())
edit.beginEditBlock()
edit.setPosition(start)
edit.setPosition(start + 2, QTextCursor.KeepAnchor)
edit.insertText("```\n\n```")
edit.endEditBlock()
# place caret on the blank line between the fences
new_pos = start + 4 # after "```\n"
c.setPosition(new_pos)
self.setTextCursor(c)
return
# Handle Enter key for smart list continuation AND code blocks
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
cursor = self.textCursor()
@ -525,34 +593,73 @@ class MarkdownEditor(QTextEdit):
super().keyPressEvent(event)
def mousePressEvent(self, event):
"""Handle mouse clicks - check for checkbox clicking."""
if event.button() == Qt.MouseButton.LeftButton:
cursor = self.cursorForPosition(event.pos())
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
line = cursor.selectedText()
"""Toggle a checkbox only when the click lands on its icon."""
if event.button() == Qt.LeftButton:
pt = event.pos()
# Check if clicking on a checkbox line
if (
f"{self._CHECK_UNCHECKED_DISPLAY} " in line
or f"{self._CHECK_CHECKED_DISPLAY} " in line
):
# Toggle the checkbox
if f"{self._CHECK_UNCHECKED_DISPLAY} " in line:
new_line = line.replace(
f"{self._CHECK_UNCHECKED_DISPLAY} ",
f"{self._CHECK_CHECKED_DISPLAY} ",
# Cursor and block under the mouse
cur = self.cursorForPosition(pt)
block = cur.block()
text = block.text()
# The display tokens, e.g. "☐ " / "☑ " (icon + trailing space)
unchecked = f"{self._CHECK_UNCHECKED_DISPLAY} "
checked = f"{self._CHECK_CHECKED_DISPLAY} "
# Helper: rect for a single character at a given doc position
def char_rect_at(doc_pos, ch):
c = QTextCursor(self.document())
c.setPosition(doc_pos)
start_rect = self.cursorRect(
c
) # caret rect at char start (viewport coords)
# Use the actual font at this position for an accurate width
fmt_font = (
c.charFormat().font() if c.charFormat().isValid() else self.font()
)
fm = QFontMetrics(fmt_font)
w = max(1, fm.horizontalAdvance(ch))
return QRect(start_rect.x(), start_rect.y(), w, start_rect.height())
# Scan the line for any checkbox icons; toggle the one we clicked
i = 0
while i < len(text):
icon = None
if text.startswith(unchecked, i):
icon = self._CHECK_UNCHECKED_DISPLAY
elif text.startswith(checked, i):
icon = self._CHECK_CHECKED_DISPLAY
if icon:
doc_pos = (
block.position() + i
) # absolute document position of the icon
r = char_rect_at(doc_pos, icon)
if r.contains(pt):
# Build the replacement: swap ☐ <-> ☑ (keep trailing space)
new_icon = (
self._CHECK_CHECKED_DISPLAY
if icon == self._CHECK_UNCHECKED_DISPLAY
else self._CHECK_UNCHECKED_DISPLAY
)
edit = QTextCursor(self.document())
edit.beginEditBlock()
edit.setPosition(doc_pos)
edit.movePosition(
QTextCursor.Right, QTextCursor.KeepAnchor, len(icon) + 1
) # icon + space
edit.insertText(f"{new_icon} ")
edit.endEditBlock()
return # handled
# advance past this token
i += len(icon) + 1
else:
new_line = line.replace(
f"{self._CHECK_CHECKED_DISPLAY} ",
f"{self._CHECK_UNCHECKED_DISPLAY} ",
)
i += 1
cursor.insertText(new_line)
# Don't call super() - we handled the click
return
# Default handling for non-checkbox clicks
# Default handling for anything else
super().mousePressEvent(event)
# ------------------------ Toolbar action handlers ------------------------

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "bouquin"
version = "0.2.1.2"
version = "0.2.1.3"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md"