bouquin 0.2.1.2__py3-none-any.whl → 0.2.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ from PySide6.QtGui import (
8
8
  QColor,
9
9
  QFont,
10
10
  QFontDatabase,
11
+ QFontMetrics,
11
12
  QImage,
12
13
  QPalette,
13
14
  QGuiApplication,
@@ -17,7 +18,7 @@ from PySide6.QtGui import (
17
18
  QSyntaxHighlighter,
18
19
  QTextImageFormat,
19
20
  )
20
- from PySide6.QtCore import Qt
21
+ from PySide6.QtCore import Qt, QRect
21
22
  from PySide6.QtWidgets import QTextEdit
22
23
 
23
24
  from .theme import ThemeManager, Theme
@@ -33,6 +34,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
33
34
  # Recompute formats whenever the app theme changes
34
35
  try:
35
36
  self.theme_manager.themeChanged.connect(self._on_theme_changed)
37
+ self.textChanged.connect(self._refresh_codeblock_margins)
36
38
  except Exception:
37
39
  pass
38
40
 
@@ -64,6 +66,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
64
66
  self.code_block_format = QTextCharFormat()
65
67
  self.code_block_format.setFont(mono)
66
68
  self.code_block_format.setFontFixedPitch(True)
69
+
67
70
  pal = QGuiApplication.palette()
68
71
  if self.theme_manager.current() == Theme.DARK:
69
72
  # In dark mode, use a darker panel-like background
@@ -96,6 +99,36 @@ class MarkdownHighlighter(QSyntaxHighlighter):
96
99
  # Also make them very faint in case they still show
97
100
  self.syntax_format.setForeground(QColor(250, 250, 250))
98
101
 
102
+ def _refresh_codeblock_margins(self):
103
+ """Give code blocks a small left/right margin to separate them visually."""
104
+ doc = self.document()
105
+ block = doc.begin()
106
+ in_code = False
107
+ while block.isValid():
108
+ txt = block.text().strip()
109
+ cursor = QTextCursor(block)
110
+ fmt = block.blockFormat()
111
+
112
+ if txt.startswith("```"):
113
+ # fence lines: small vertical spacing, same left indent
114
+ need = (12, 6, 6) # left, top, bottom (px-like)
115
+ if (fmt.leftMargin(), fmt.topMargin(), fmt.bottomMargin()) != need:
116
+ fmt.setLeftMargin(12)
117
+ fmt.setRightMargin(6)
118
+ fmt.setTopMargin(6)
119
+ fmt.setBottomMargin(6)
120
+ cursor.setBlockFormat(fmt)
121
+ in_code = not in_code
122
+
123
+ elif in_code:
124
+ # inside the code block
125
+ if fmt.leftMargin() != 12 or fmt.rightMargin() != 6:
126
+ fmt.setLeftMargin(12)
127
+ fmt.setRightMargin(6)
128
+ cursor.setBlockFormat(fmt)
129
+
130
+ block = block.next()
131
+
99
132
  def highlightBlock(self, text: str):
100
133
  """Apply formatting to a block of text based on markdown syntax."""
101
134
  if not text:
@@ -107,12 +140,17 @@ class MarkdownHighlighter(QSyntaxHighlighter):
107
140
 
108
141
  # Check for code block fences
109
142
  if text.strip().startswith("```"):
110
- # Toggle code block state
143
+ # background for the whole fence line (so block looks continuous)
144
+ self.setFormat(0, len(text), self.code_block_format)
145
+
146
+ # hide the three backticks themselves
147
+ idx = text.find("```")
148
+ if idx != -1:
149
+ self.setFormat(idx, 3, self.syntax_format)
150
+
151
+ # toggle code-block state and stop; next line picks up state
111
152
  in_code_block = not in_code_block
112
153
  self.setCurrentBlockState(1 if in_code_block else 0)
113
- # Format the fence markers - but keep them somewhat visible for editing
114
- # Use code format instead of syntax format so cursor is visible
115
- self.setFormat(0, len(text), self.code_format)
116
154
  return
117
155
 
118
156
  if in_code_block:
@@ -447,6 +485,36 @@ class MarkdownEditor(QTextEdit):
447
485
  def keyPressEvent(self, event):
448
486
  """Handle special key events for markdown editing."""
449
487
 
488
+ # --- Auto-close code fences when typing the 3rd backtick at line start ---
489
+ if event.text() == "`":
490
+ c = self.textCursor()
491
+ block = c.block()
492
+ line = block.text()
493
+ pos_in_block = c.position() - block.position()
494
+
495
+ # text before caret on this line
496
+ before = line[:pos_in_block]
497
+
498
+ # If we've typed exactly two backticks at line start (or after whitespace),
499
+ # treat this backtick as the "third" and expand to a full fenced block.
500
+ if before.endswith("``") and before.strip() == "``":
501
+ start = (
502
+ block.position() + pos_in_block - 2
503
+ ) # start of the two backticks
504
+
505
+ edit = QTextCursor(self.document())
506
+ edit.beginEditBlock()
507
+ edit.setPosition(start)
508
+ edit.setPosition(start + 2, QTextCursor.KeepAnchor)
509
+ edit.insertText("```\n\n```")
510
+ edit.endEditBlock()
511
+
512
+ # place caret on the blank line between the fences
513
+ new_pos = start + 4 # after "```\n"
514
+ c.setPosition(new_pos)
515
+ self.setTextCursor(c)
516
+ return
517
+
450
518
  # Handle Enter key for smart list continuation AND code blocks
451
519
  if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
452
520
  cursor = self.textCursor()
@@ -525,34 +593,73 @@ class MarkdownEditor(QTextEdit):
525
593
  super().keyPressEvent(event)
526
594
 
527
595
  def mousePressEvent(self, event):
528
- """Handle mouse clicks - check for checkbox clicking."""
529
- if event.button() == Qt.MouseButton.LeftButton:
530
- cursor = self.cursorForPosition(event.pos())
531
- cursor.select(QTextCursor.SelectionType.LineUnderCursor)
532
- line = cursor.selectedText()
596
+ """Toggle a checkbox only when the click lands on its icon."""
597
+ if event.button() == Qt.LeftButton:
598
+ pt = event.pos()
599
+
600
+ # Cursor and block under the mouse
601
+ cur = self.cursorForPosition(pt)
602
+ block = cur.block()
603
+ text = block.text()
604
+
605
+ # The display tokens, e.g. "☐ " / "☑ " (icon + trailing space)
606
+ unchecked = f"{self._CHECK_UNCHECKED_DISPLAY} "
607
+ checked = f"{self._CHECK_CHECKED_DISPLAY} "
608
+
609
+ # Helper: rect for a single character at a given doc position
610
+ def char_rect_at(doc_pos, ch):
611
+ c = QTextCursor(self.document())
612
+ c.setPosition(doc_pos)
613
+ start_rect = self.cursorRect(
614
+ c
615
+ ) # caret rect at char start (viewport coords)
616
+
617
+ # Use the actual font at this position for an accurate width
618
+ fmt_font = (
619
+ c.charFormat().font() if c.charFormat().isValid() else self.font()
620
+ )
621
+ fm = QFontMetrics(fmt_font)
622
+ w = max(1, fm.horizontalAdvance(ch))
623
+ return QRect(start_rect.x(), start_rect.y(), w, start_rect.height())
533
624
 
534
- # Check if clicking on a checkbox line
535
- if (
536
- f"{self._CHECK_UNCHECKED_DISPLAY} " in line
537
- or f"{self._CHECK_CHECKED_DISPLAY} " in line
538
- ):
539
- # Toggle the checkbox
540
- if f"{self._CHECK_UNCHECKED_DISPLAY} " in line:
541
- new_line = line.replace(
542
- f"{self._CHECK_UNCHECKED_DISPLAY} ",
543
- f"{self._CHECK_CHECKED_DISPLAY} ",
544
- )
625
+ # Scan the line for any checkbox icons; toggle the one we clicked
626
+ i = 0
627
+ while i < len(text):
628
+ icon = None
629
+ if text.startswith(unchecked, i):
630
+ icon = self._CHECK_UNCHECKED_DISPLAY
631
+ elif text.startswith(checked, i):
632
+ icon = self._CHECK_CHECKED_DISPLAY
633
+
634
+ if icon:
635
+ doc_pos = (
636
+ block.position() + i
637
+ ) # absolute document position of the icon
638
+ r = char_rect_at(doc_pos, icon)
639
+
640
+ if r.contains(pt):
641
+ # Build the replacement: swap ☐ <-> ☑ (keep trailing space)
642
+ new_icon = (
643
+ self._CHECK_CHECKED_DISPLAY
644
+ if icon == self._CHECK_UNCHECKED_DISPLAY
645
+ else self._CHECK_UNCHECKED_DISPLAY
646
+ )
647
+ edit = QTextCursor(self.document())
648
+ edit.beginEditBlock()
649
+ edit.setPosition(doc_pos)
650
+ edit.movePosition(
651
+ QTextCursor.Right, QTextCursor.KeepAnchor, len(icon) + 1
652
+ ) # icon + space
653
+ edit.insertText(f"{new_icon} ")
654
+ edit.endEditBlock()
655
+ return # handled
656
+
657
+ # advance past this token
658
+ i += len(icon) + 1
545
659
  else:
546
- new_line = line.replace(
547
- f"{self._CHECK_CHECKED_DISPLAY} ",
548
- f"{self._CHECK_UNCHECKED_DISPLAY} ",
549
- )
550
-
551
- cursor.insertText(new_line)
552
- # Don't call super() - we handled the click
553
- return
660
+ i += 1
554
661
 
555
- # Default handling for non-checkbox clicks
662
+ # Default handling for anything else
556
663
  super().mousePressEvent(event)
557
664
 
558
665
  # ------------------------ Toolbar action handlers ------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bouquin
3
- Version: 0.2.1.2
3
+ Version: 0.2.1.3
4
4
  Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
5
5
  Home-page: https://git.mig5.net/mig5/bouquin
6
6
  License: GPL-3.0-or-later
@@ -7,15 +7,15 @@ bouquin/key_prompt.py,sha256=oQhLDOQv1QUr_ImA9Zu78JkDpVqPbZZJdhu0c_5Cq5U,1266
7
7
  bouquin/lock_overlay.py,sha256=d1xoBMx2CSNk0zP5V6k65UqJCC4aiIrwNlfDld49ymA,4197
8
8
  bouquin/main.py,sha256=lBOMS7THgHb4CAJVj8NRYABtNAEez9jlL0wI1oOtfT4,611
9
9
  bouquin/main_window.py,sha256=idyUFj_lNlmSCZgfYecV16JMzQJAm2ZcmEmzNCM-jmQ,50690
10
- bouquin/markdown_editor.py,sha256=MeKe8LkFh2XtEwUnIiIshKytGmMVVOV_3F-aUVhdKyQ,29017
10
+ bouquin/markdown_editor.py,sha256=5esRhZuUEKySw2YjVPaeXOA0TOJW13OaEK1h9I_ZDdw,33318
11
11
  bouquin/save_dialog.py,sha256=YUkZ8kL1hja15D8qv68yY2zPyjBAJZsDQbp6Y6EvDbA,1023
12
12
  bouquin/search.py,sha256=jUhiy90pThmQUvsDnKYjes5SRnAWKBkqGpHPWMsfRJs,7764
13
13
  bouquin/settings.py,sha256=F3WLkk2G_By3ppZsRbrnq3PtL2Zav7aA-mIegvGTc8Y,1128
14
14
  bouquin/settings_dialog.py,sha256=YQHYjn3y2sgJGtkkApADxAodojDfLvnZYeQmynXLkos,10699
15
15
  bouquin/theme.py,sha256=6ODq9oKLAk7lnvW9uGRMIIjfhf70SgPivLYh3ZN671M,3489
16
16
  bouquin/toolbar.py,sha256=_FZ4lqJmQD86qx-0k7XpAu3HzyFedX2pG2mGyug0ea8,6588
17
- bouquin-0.2.1.2.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
18
- bouquin-0.2.1.2.dist-info/METADATA,sha256=LAbzz9A-S-ymz6cHn-oZ8IeksoxlKZiGc5qpc71Ugzs,3141
19
- bouquin-0.2.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
- bouquin-0.2.1.2.dist-info/entry_points.txt,sha256=d2C5Mc85suj1vWg_mmcfFuEBAYEkdwhZquusme5EWuQ,49
21
- bouquin-0.2.1.2.dist-info/RECORD,,
17
+ bouquin-0.2.1.3.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
18
+ bouquin-0.2.1.3.dist-info/METADATA,sha256=S2vVxGNzrff0_pLwOBfzXjIR3DEWG0Fhjt0IVFzNt-8,3141
19
+ bouquin-0.2.1.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
+ bouquin-0.2.1.3.dist-info/entry_points.txt,sha256=d2C5Mc85suj1vWg_mmcfFuEBAYEkdwhZquusme5EWuQ,49
21
+ bouquin-0.2.1.3.dist-info/RECORD,,