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.
- bouquin/markdown_editor.py +137 -30
- {bouquin-0.2.1.2.dist-info → bouquin-0.2.1.3.dist-info}/METADATA +1 -1
- {bouquin-0.2.1.2.dist-info → bouquin-0.2.1.3.dist-info}/RECORD +6 -6
- {bouquin-0.2.1.2.dist-info → bouquin-0.2.1.3.dist-info}/LICENSE +0 -0
- {bouquin-0.2.1.2.dist-info → bouquin-0.2.1.3.dist-info}/WHEEL +0 -0
- {bouquin-0.2.1.2.dist-info → bouquin-0.2.1.3.dist-info}/entry_points.txt +0 -0
bouquin/markdown_editor.py
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
"""
|
|
529
|
-
if event.button() == Qt.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
#
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
|
662
|
+
# Default handling for anything else
|
|
556
663
|
super().mousePressEvent(event)
|
|
557
664
|
|
|
558
665
|
# ------------------------ Toolbar action handlers ------------------------
|
|
@@ -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=
|
|
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.
|
|
18
|
-
bouquin-0.2.1.
|
|
19
|
-
bouquin-0.2.1.
|
|
20
|
-
bouquin-0.2.1.
|
|
21
|
-
bouquin-0.2.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|