diff --git a/CHANGELOG.md b/CHANGELOG.md index 63af14a..2faf357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,3 @@ -# 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 diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index baea055..d864770 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -8,7 +8,6 @@ from PySide6.QtGui import ( QColor, QFont, QFontDatabase, - QFontMetrics, QImage, QPalette, QGuiApplication, @@ -18,7 +17,7 @@ from PySide6.QtGui import ( QSyntaxHighlighter, QTextImageFormat, ) -from PySide6.QtCore import Qt, QRect +from PySide6.QtCore import Qt from PySide6.QtWidgets import QTextEdit from .theme import ThemeManager, Theme @@ -34,7 +33,6 @@ 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 @@ -66,7 +64,6 @@ 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 @@ -99,36 +96,6 @@ 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: @@ -140,17 +107,12 @@ class MarkdownHighlighter(QSyntaxHighlighter): # Check for code block fences if text.strip().startswith("```"): - # 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 + # Toggle code block 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: @@ -485,36 +447,6 @@ 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() @@ -593,73 +525,34 @@ class MarkdownEditor(QTextEdit): super().keyPressEvent(event) def mousePressEvent(self, event): - """Toggle a checkbox only when the click lands on its icon.""" - if event.button() == Qt.LeftButton: - pt = event.pos() + """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() - # 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 + # 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} ", + ) else: - i += 1 + new_line = line.replace( + f"{self._CHECK_CHECKED_DISPLAY} ", + f"{self._CHECK_UNCHECKED_DISPLAY} ", + ) - # Default handling for anything else + cursor.insertText(new_line) + # Don't call super() - we handled the click + return + + # Default handling for non-checkbox clicks super().mousePressEvent(event) # ------------------------ Toolbar action handlers ------------------------ diff --git a/pyproject.toml b/pyproject.toml index df93682..5de25b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.2.1.3" +version = "0.2.1.2" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md"