Compare commits
2 commits
dfde0d6e6c
...
4f4735cfb6
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f4735cfb6 | |||
| bc9fa86281 |
3 changed files with 143 additions and 31 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ------------------------
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue