Skip to content

checkbox_handler

Checkbox handler for markdown checkboxes in QTextEdit.

CheckboxHandler

Bases: QObject

Handles checkbox parsing, rendering, and state management.

This class manages: - Parsing markdown to extract checkbox items - Registering custom text objects for checkbox rendering - Tracking checkbox state and positions - Converting back to markdown format

Signals

checklist_changed: Emitted when any checkbox state changes.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
class CheckboxHandler(QObject):
    """Handles checkbox parsing, rendering, and state management.

    This class manages:
    - Parsing markdown to extract checkbox items
    - Registering custom text objects for checkbox rendering
    - Tracking checkbox state and positions
    - Converting back to markdown format

    Signals:
        checklist_changed: Emitted when any checkbox state changes.
    """

    checklist_changed = Signal()

    # GitHub-flavored markdown checkbox patterns
    # Matches: - [ ] text, - [x] text, * [ ] text, * [X] text, + [ ] text
    CHECKBOX_PATTERN = re.compile(
        r"^(\s*[-*+]\s+)\[([xX ])\]\s*(.*)$", re.MULTILINE
    )

    def __init__(self, text_edit: QTextEdit, parent: QObject | None = None):
        """Initialize the checkbox handler.

        Args:
            text_edit: The QTextEdit widget to manage checkboxes for.
            parent: Optional parent QObject.
        """
        super().__init__(parent)
        self._text_edit = text_edit
        self._checkboxes: List[CheckboxItem] = []
        self._checkbox_object: CheckboxTextObject | None = None
        self._original_markdown: str = ""
        self._register_handler()

    def _register_handler(self) -> None:
        """Register the custom checkbox text object with the document."""
        self._checkbox_object = CheckboxTextObject(self)
        doc = self._text_edit.document()
        doc_layout = doc.documentLayout()
        doc_layout.registerHandler(CHECKBOX_FORMAT_TYPE, self._checkbox_object)

    def parse_and_render(self, markdown: str) -> None:
        """Parse markdown and render checkboxes in the document.

        Extracts checkbox items from markdown, stores their state,
        and renders them as custom text objects with icons.

        Args:
            markdown: The markdown string containing checkboxes.
        """
        self._original_markdown = markdown
        self._checkboxes.clear()

        # Find all checkbox patterns
        lines = markdown.split("\n")
        processed_lines: List[str] = []
        checkbox_index = 0

        for line_num, line in enumerate(lines):
            match = self.CHECKBOX_PATTERN.match(line)
            if match:
                prefix = match.group(1)
                checked_char = match.group(2)
                text = match.group(3)
                is_checked = checked_char.lower() == "x"

                # Store checkbox info (doc_position filled after insertion)
                self._checkboxes.append(
                    CheckboxItem(
                        index=checkbox_index,
                        checked=is_checked,
                        text=text,
                        line_number=line_num,
                        prefix=prefix,
                    )
                )

                # Replace checkbox syntax with placeholder
                # We'll insert the actual object after setting plain text
                processed_lines.append(f"{prefix}\ufffc {text}")
                checkbox_index += 1
            else:
                processed_lines.append(line)

        # Set the processed text
        processed_text = "\n".join(processed_lines)
        self._text_edit.setMarkdown(processed_text)
        self._remove_list_bullets()

        # Now insert checkbox objects at placeholder positions
        self._insert_checkbox_objects()

    def _remove_list_bullets(self) -> None:
        """Remove bullet markers from list blocks containing checkboxes and
        correct the indentation to match front-end."""
        doc = self._text_edit.document()
        processed_lists: set[int] = set()

        block = doc.begin()
        while block.isValid():
            text_list = block.textList()
            if text_list and id(text_list) not in processed_lists:
                list_key = text_list.item(0).position()
                if "\ufffc" in block.text():
                    fmt = text_list.format()
                    fmt.setStyle(QTextListFormat.Style.ListStyleUndefined)
                    fmt.setIndent(0)
                    text_list.setFormat(fmt)
                    processed_lists.add(list_key)
            block = block.next()

    def _insert_checkbox_objects(self) -> None:
        """Insert checkbox custom objects at placeholder positions."""
        doc = self._text_edit.document()
        doc.blockSignals(True)

        cursor = QTextCursor(doc)
        cursor.beginEditBlock()

        # Find all placeholder characters and replace with checkbox objects
        text = doc.toPlainText()
        offset = 0

        for i, cb in enumerate(self._checkboxes):
            # Find the placeholder character (OBJECT REPLACEMENT CHARACTER)
            pos = text.find("\ufffc", offset)
            if pos == -1:
                log.warning("Could not find placeholder for checkbox %d", i)
                continue

            # Create format for the checkbox
            fmt = QTextCharFormat()
            fmt.setObjectType(CHECKBOX_FORMAT_TYPE)
            fmt.setProperty(CHECKBOX_CHECKED_PROP, cb.checked)
            fmt.setProperty(CHECKBOX_INDEX_PROP, cb.index)
            fmt.setVerticalAlignment(
                QTextCharFormat.VerticalAlignment.AlignBaseline
            )

            # Select and replace the placeholder
            cursor.setPosition(pos)
            cursor.setPosition(pos + 1, QTextCursor.MoveMode.KeepAnchor)
            cursor.insertText("\ufffc", fmt)

            # Store the document position for fast hit-testing
            cb.doc_position = pos

            offset = pos + 1

        cursor.endEditBlock()
        doc.blockSignals(False)

    def get_checkbox_at_position(self, pos: int) -> int | None:
        """Get the checkbox index at the given document position.

        Args:
            pos: Position in the document (one past the \\ufffc char).

        Returns:
            Checkbox index if found, None otherwise.
        """
        doc = self._text_edit.document()
        cursor = QTextCursor(doc)
        cursor.setPosition(pos)
        fmt = cursor.charFormat()
        fmt_type = fmt.objectType()

        if fmt_type == CHECKBOX_FORMAT_TYPE:
            return fmt.property(CHECKBOX_INDEX_PROP)
        return None

    def find_checkbox_at_click(
        self, click_pos: "QPoint", content_offset: "QPoint | None" = None
    ) -> Optional[Tuple[bool, Optional[int]]]:
        """Find the checkbox hit by a viewport click using stored positions.

        Uses the cached ``doc_position`` on each :class:`CheckboxItem`
        instead of scanning the full document text, making hit-testing O(n)
        in the number of checkboxes rather than O(len(document)).

        Args:
            click_pos: Click position in viewport coordinates.
            content_offset: Optional scroll offset (x, y) as a QPoint.
                Defaults to (0, 0) when not provided.

        Returns:
            ``(True, doc_position + 1)`` when the click lands on a checkbox,
            ``(False, None)`` otherwise.
        """
        if not self._checkboxes:
            return False, None

        doc = self._text_edit.document()
        ox = content_offset.x() if content_offset else 0
        oy = content_offset.y() if content_offset else 0

        for cb in self._checkboxes:
            pos = cb.doc_position
            if pos < 0:
                continue

            cursor = QTextCursor(doc)
            cursor.setPosition(pos)
            block = cursor.block()
            block_layout = block.layout()

            if not block_layout:
                continue

            rel_pos = pos - block.position()
            line = block_layout.lineForTextPosition(rel_pos)
            if not line.isValid():
                continue

            x1 = line.cursorToX(rel_pos)[0]  # type: ignore[index]
            x2 = line.cursorToX(rel_pos + 1)[0]  # type: ignore[index]
            y = line.y() + block_layout.position().y()
            h = line.height()

            rect = QRectF(x1 + ox, y + oy, x2 - x1, h)
            if rect.contains(QPointF(click_pos)):
                return True, pos + 1

        return False, None

    def toggle_checkbox(self, index: int) -> bool:
        """Toggle a checkbox by its index.

        Args:
            index: The index of the checkbox to toggle.

        Returns:
            True if the checkbox was toggled, False otherwise.
        """
        if not 0 <= index < len(self._checkboxes):
            return False

        # Toggle the state
        self._checkboxes[index].checked = not self._checkboxes[index].checked

        # Update the document
        self._update_checkbox_in_document(index)

        # Emit signal
        self.checklist_changed.emit()
        return True

    def _update_checkbox_in_document(self, index: int) -> None:
        """Update a single checkbox display in the document.

        Args:
            index: The index of the checkbox to update.
        """
        doc = self._text_edit.document()
        cb = self._checkboxes[index]

        # Block signals to prevent triggering format_comment_on_change
        # which would clear all formatting including checkboxes
        doc.blockSignals(True)

        try:
            pos = cb.doc_position
            if pos < 0:
                # Fall back to scanning if position is unknown
                text = doc.toPlainText()
                count = 0
                for i, char in enumerate(text):
                    if char == "\ufffc":
                        if count == index:
                            pos = i
                            break
                        count += 1

            if pos < 0:
                log.warning(
                    "Cannot find document position for checkbox %d", index
                )
                return

            cursor = QTextCursor(doc)
            cursor.setPosition(pos)
            cursor.setPosition(pos + 1, QTextCursor.MoveMode.KeepAnchor)

            fmt = QTextCharFormat()
            fmt.setObjectType(CHECKBOX_FORMAT_TYPE)
            fmt.setProperty(CHECKBOX_CHECKED_PROP, cb.checked)
            fmt.setProperty(CHECKBOX_INDEX_PROP, cb.index)
            fmt.setVerticalAlignment(
                QTextCharFormat.VerticalAlignment.AlignBaseline
            )

            cursor.setCharFormat(fmt)
        finally:
            doc.blockSignals(False)

    def add_checkbox(
        self,
        checked: bool = False,
        prefix: str = "- ",
        doc_position: int = -1,
    ) -> CheckboxItem:
        """Append a new checkbox to the tracked list.

        This is the public API for adding a checkbox programmatically
        (e.g. when the user presses Enter on a checkbox line or clicks
        the checklist toolbar button).  The caller is responsible for
        inserting the actual ``\\ufffc`` character into the document and
        then updating :attr:`CheckboxItem.doc_position`.

        Args:
            checked: Initial checked state.
            prefix: Markdown list prefix (e.g. ``"- "``).
            doc_position: Document character position of the ``\\ufffc``
                object, or ``-1`` if not yet known.

        Returns:
            The newly created :class:`CheckboxItem`.
        """
        new_index = len(self._checkboxes)
        item = CheckboxItem(
            index=new_index,
            checked=checked,
            text="",
            line_number=-1,
            prefix=prefix,
            doc_position=doc_position,
        )
        self._checkboxes.append(item)
        return item

    def remove_checkbox(self, index: int) -> None:
        """Remove a checkbox from the tracked list.

        Re-indexes the Python list **and** patches the stale
        ``CHECKBOX_INDEX_PROP`` values stored in the live
        ``QTextDocument`` so that subsequent hit-tests via
        :meth:`get_checkbox_at_position` return correct indices.

        Args:
            index: The index of the checkbox to remove.
        """
        if not 0 <= index < len(self._checkboxes):
            return

        self._checkboxes.pop(index)

        # Re-index the Python list and sync each affected document char format.
        doc = self._text_edit.document()
        doc.blockSignals(True)
        cursor = QTextCursor(doc)
        cursor.beginEditBlock()
        try:
            for i, cb in enumerate(self._checkboxes[index:], start=index):
                cb.index = i
                pos = cb.doc_position
                if pos < 0:
                    continue
                cursor.setPosition(pos)
                cursor.setPosition(pos + 1, QTextCursor.MoveMode.KeepAnchor)
                fmt = QTextCharFormat()
                fmt.setObjectType(CHECKBOX_FORMAT_TYPE)
                fmt.setProperty(CHECKBOX_CHECKED_PROP, cb.checked)
                fmt.setProperty(CHECKBOX_INDEX_PROP, cb.index)
                fmt.setVerticalAlignment(
                    QTextCharFormat.VerticalAlignment.AlignBaseline
                )
                cursor.setCharFormat(fmt)
        except Exception:
            log.warning(
                "Failed to re-index checkboxes in document", exc_info=True
            )
        finally:
            cursor.endEditBlock()
            doc.blockSignals(False)

    def remove_last_checkbox(self) -> bool:
        """Remove the last checkbox from the tracked list.

        Used when the user presses Enter on an empty checkbox line to
        terminate the list.

        Returns:
            True if a checkbox was removed, False if the list was empty.
        """
        if not self._checkboxes:
            return False
        self._checkboxes.pop()
        return True

    def to_markdown(self) -> str:
        """Reconstruct markdown with current checkbox states.

        Returns:
            GitHub-flavored markdown string with checkbox syntax.
        """
        if not self._checkboxes:
            return self._text_edit.toMarkdown(MD_DIALECT)

        # Get current text and replace placeholders with checkbox syntax
        # we use toMarkdown() to make sure the other formatted items have
        # already been dealt with.
        text = self._text_edit.toMarkdown(MD_DIALECT)
        lines = text.split("\n")
        result_lines: List[str] = []

        checkbox_iter = iter(self._checkboxes)
        current_cb = next(checkbox_iter, None)

        for line in lines:
            if "\ufffc" in line and current_cb:
                checked_char = "x" if current_cb.checked else " "
                cb_idx = line.index("\ufffc")
                raw_after = line[cb_idx + 1:]       # everything after \ufffc
                raw_after = raw_after.lstrip(" ")    # drop the space Qt inserts
                raw_after = _PH_RE.sub("", raw_after).rstrip()
                line = f"{current_cb.prefix}[{checked_char}] {raw_after}"
                current_cb = next(checkbox_iter, None)
            result_lines.append(line)

        return "\n".join(result_lines)

    @property
    def checkboxes(self) -> List[CheckboxItem]:
        """Get the list of checkbox items.

        Returns:
            List of CheckboxItem objects.
        """
        return self._checkboxes.copy()

    def has_checkboxes(self) -> bool:
        """Check if the document contains any checkboxes.

        Returns:
            True if there are checkboxes, False otherwise.
        """
        return bool(self._checkboxes)

    @staticmethod
    def contains_checkboxes(markdown: str) -> bool:
        """Check if markdown text contains checkbox syntax.

        Args:
            markdown: The markdown string to check.

        Returns:
            True if checkbox syntax is found, False otherwise.
        """
        return bool(CheckboxHandler.CHECKBOX_PATTERN.search(markdown))

checkboxes property

Get the list of checkbox items.

Returns:

Type Description
List[CheckboxItem]

List of CheckboxItem objects.

__init__(text_edit, parent=None)

Initialize the checkbox handler.

Parameters:

Name Type Description Default
text_edit QTextEdit

The QTextEdit widget to manage checkboxes for.

required
parent QObject | None

Optional parent QObject.

None
Source code in client/ayon_ui_qt/components/checkbox_handler.py
160
161
162
163
164
165
166
167
168
169
170
171
172
def __init__(self, text_edit: QTextEdit, parent: QObject | None = None):
    """Initialize the checkbox handler.

    Args:
        text_edit: The QTextEdit widget to manage checkboxes for.
        parent: Optional parent QObject.
    """
    super().__init__(parent)
    self._text_edit = text_edit
    self._checkboxes: List[CheckboxItem] = []
    self._checkbox_object: CheckboxTextObject | None = None
    self._original_markdown: str = ""
    self._register_handler()

add_checkbox(checked=False, prefix='- ', doc_position=-1)

Append a new checkbox to the tracked list.

This is the public API for adding a checkbox programmatically (e.g. when the user presses Enter on a checkbox line or clicks the checklist toolbar button). The caller is responsible for inserting the actual \ufffc character into the document and then updating :attr:CheckboxItem.doc_position.

Parameters:

Name Type Description Default
checked bool

Initial checked state.

False
prefix str

Markdown list prefix (e.g. "- ").

'- '
doc_position int

Document character position of the \ufffc object, or -1 if not yet known.

-1

Returns:

Type Description
CheckboxItem

The newly created :class:CheckboxItem.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
def add_checkbox(
    self,
    checked: bool = False,
    prefix: str = "- ",
    doc_position: int = -1,
) -> CheckboxItem:
    """Append a new checkbox to the tracked list.

    This is the public API for adding a checkbox programmatically
    (e.g. when the user presses Enter on a checkbox line or clicks
    the checklist toolbar button).  The caller is responsible for
    inserting the actual ``\\ufffc`` character into the document and
    then updating :attr:`CheckboxItem.doc_position`.

    Args:
        checked: Initial checked state.
        prefix: Markdown list prefix (e.g. ``"- "``).
        doc_position: Document character position of the ``\\ufffc``
            object, or ``-1`` if not yet known.

    Returns:
        The newly created :class:`CheckboxItem`.
    """
    new_index = len(self._checkboxes)
    item = CheckboxItem(
        index=new_index,
        checked=checked,
        text="",
        line_number=-1,
        prefix=prefix,
        doc_position=doc_position,
    )
    self._checkboxes.append(item)
    return item

contains_checkboxes(markdown) staticmethod

Check if markdown text contains checkbox syntax.

Parameters:

Name Type Description Default
markdown str

The markdown string to check.

required

Returns:

Type Description
bool

True if checkbox syntax is found, False otherwise.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
578
579
580
581
582
583
584
585
586
587
588
@staticmethod
def contains_checkboxes(markdown: str) -> bool:
    """Check if markdown text contains checkbox syntax.

    Args:
        markdown: The markdown string to check.

    Returns:
        True if checkbox syntax is found, False otherwise.
    """
    return bool(CheckboxHandler.CHECKBOX_PATTERN.search(markdown))

find_checkbox_at_click(click_pos, content_offset=None)

Find the checkbox hit by a viewport click using stored positions.

Uses the cached doc_position on each :class:CheckboxItem instead of scanning the full document text, making hit-testing O(n) in the number of checkboxes rather than O(len(document)).

Parameters:

Name Type Description Default
click_pos 'QPoint'

Click position in viewport coordinates.

required
content_offset 'QPoint | None'

Optional scroll offset (x, y) as a QPoint. Defaults to (0, 0) when not provided.

None

Returns:

Type Description
Optional[Tuple[bool, Optional[int]]]

(True, doc_position + 1) when the click lands on a checkbox,

Optional[Tuple[bool, Optional[int]]]

(False, None) otherwise.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def find_checkbox_at_click(
    self, click_pos: "QPoint", content_offset: "QPoint | None" = None
) -> Optional[Tuple[bool, Optional[int]]]:
    """Find the checkbox hit by a viewport click using stored positions.

    Uses the cached ``doc_position`` on each :class:`CheckboxItem`
    instead of scanning the full document text, making hit-testing O(n)
    in the number of checkboxes rather than O(len(document)).

    Args:
        click_pos: Click position in viewport coordinates.
        content_offset: Optional scroll offset (x, y) as a QPoint.
            Defaults to (0, 0) when not provided.

    Returns:
        ``(True, doc_position + 1)`` when the click lands on a checkbox,
        ``(False, None)`` otherwise.
    """
    if not self._checkboxes:
        return False, None

    doc = self._text_edit.document()
    ox = content_offset.x() if content_offset else 0
    oy = content_offset.y() if content_offset else 0

    for cb in self._checkboxes:
        pos = cb.doc_position
        if pos < 0:
            continue

        cursor = QTextCursor(doc)
        cursor.setPosition(pos)
        block = cursor.block()
        block_layout = block.layout()

        if not block_layout:
            continue

        rel_pos = pos - block.position()
        line = block_layout.lineForTextPosition(rel_pos)
        if not line.isValid():
            continue

        x1 = line.cursorToX(rel_pos)[0]  # type: ignore[index]
        x2 = line.cursorToX(rel_pos + 1)[0]  # type: ignore[index]
        y = line.y() + block_layout.position().y()
        h = line.height()

        rect = QRectF(x1 + ox, y + oy, x2 - x1, h)
        if rect.contains(QPointF(click_pos)):
            return True, pos + 1

    return False, None

get_checkbox_at_position(pos)

Get the checkbox index at the given document position.

Parameters:

Name Type Description Default
pos int

Position in the document (one past the \ufffc char).

required

Returns:

Type Description
int | None

Checkbox index if found, None otherwise.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def get_checkbox_at_position(self, pos: int) -> int | None:
    """Get the checkbox index at the given document position.

    Args:
        pos: Position in the document (one past the \\ufffc char).

    Returns:
        Checkbox index if found, None otherwise.
    """
    doc = self._text_edit.document()
    cursor = QTextCursor(doc)
    cursor.setPosition(pos)
    fmt = cursor.charFormat()
    fmt_type = fmt.objectType()

    if fmt_type == CHECKBOX_FORMAT_TYPE:
        return fmt.property(CHECKBOX_INDEX_PROP)
    return None

has_checkboxes()

Check if the document contains any checkboxes.

Returns:

Type Description
bool

True if there are checkboxes, False otherwise.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
570
571
572
573
574
575
576
def has_checkboxes(self) -> bool:
    """Check if the document contains any checkboxes.

    Returns:
        True if there are checkboxes, False otherwise.
    """
    return bool(self._checkboxes)

parse_and_render(markdown)

Parse markdown and render checkboxes in the document.

Extracts checkbox items from markdown, stores their state, and renders them as custom text objects with icons.

Parameters:

Name Type Description Default
markdown str

The markdown string containing checkboxes.

required
Source code in client/ayon_ui_qt/components/checkbox_handler.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def parse_and_render(self, markdown: str) -> None:
    """Parse markdown and render checkboxes in the document.

    Extracts checkbox items from markdown, stores their state,
    and renders them as custom text objects with icons.

    Args:
        markdown: The markdown string containing checkboxes.
    """
    self._original_markdown = markdown
    self._checkboxes.clear()

    # Find all checkbox patterns
    lines = markdown.split("\n")
    processed_lines: List[str] = []
    checkbox_index = 0

    for line_num, line in enumerate(lines):
        match = self.CHECKBOX_PATTERN.match(line)
        if match:
            prefix = match.group(1)
            checked_char = match.group(2)
            text = match.group(3)
            is_checked = checked_char.lower() == "x"

            # Store checkbox info (doc_position filled after insertion)
            self._checkboxes.append(
                CheckboxItem(
                    index=checkbox_index,
                    checked=is_checked,
                    text=text,
                    line_number=line_num,
                    prefix=prefix,
                )
            )

            # Replace checkbox syntax with placeholder
            # We'll insert the actual object after setting plain text
            processed_lines.append(f"{prefix}\ufffc {text}")
            checkbox_index += 1
        else:
            processed_lines.append(line)

    # Set the processed text
    processed_text = "\n".join(processed_lines)
    self._text_edit.setMarkdown(processed_text)
    self._remove_list_bullets()

    # Now insert checkbox objects at placeholder positions
    self._insert_checkbox_objects()

remove_checkbox(index)

Remove a checkbox from the tracked list.

Re-indexes the Python list and patches the stale CHECKBOX_INDEX_PROP values stored in the live QTextDocument so that subsequent hit-tests via :meth:get_checkbox_at_position return correct indices.

Parameters:

Name Type Description Default
index int

The index of the checkbox to remove.

required
Source code in client/ayon_ui_qt/components/checkbox_handler.py
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
def remove_checkbox(self, index: int) -> None:
    """Remove a checkbox from the tracked list.

    Re-indexes the Python list **and** patches the stale
    ``CHECKBOX_INDEX_PROP`` values stored in the live
    ``QTextDocument`` so that subsequent hit-tests via
    :meth:`get_checkbox_at_position` return correct indices.

    Args:
        index: The index of the checkbox to remove.
    """
    if not 0 <= index < len(self._checkboxes):
        return

    self._checkboxes.pop(index)

    # Re-index the Python list and sync each affected document char format.
    doc = self._text_edit.document()
    doc.blockSignals(True)
    cursor = QTextCursor(doc)
    cursor.beginEditBlock()
    try:
        for i, cb in enumerate(self._checkboxes[index:], start=index):
            cb.index = i
            pos = cb.doc_position
            if pos < 0:
                continue
            cursor.setPosition(pos)
            cursor.setPosition(pos + 1, QTextCursor.MoveMode.KeepAnchor)
            fmt = QTextCharFormat()
            fmt.setObjectType(CHECKBOX_FORMAT_TYPE)
            fmt.setProperty(CHECKBOX_CHECKED_PROP, cb.checked)
            fmt.setProperty(CHECKBOX_INDEX_PROP, cb.index)
            fmt.setVerticalAlignment(
                QTextCharFormat.VerticalAlignment.AlignBaseline
            )
            cursor.setCharFormat(fmt)
    except Exception:
        log.warning(
            "Failed to re-index checkboxes in document", exc_info=True
        )
    finally:
        cursor.endEditBlock()
        doc.blockSignals(False)

remove_last_checkbox()

Remove the last checkbox from the tracked list.

Used when the user presses Enter on an empty checkbox line to terminate the list.

Returns:

Type Description
bool

True if a checkbox was removed, False if the list was empty.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
515
516
517
518
519
520
521
522
523
524
525
526
527
def remove_last_checkbox(self) -> bool:
    """Remove the last checkbox from the tracked list.

    Used when the user presses Enter on an empty checkbox line to
    terminate the list.

    Returns:
        True if a checkbox was removed, False if the list was empty.
    """
    if not self._checkboxes:
        return False
    self._checkboxes.pop()
    return True

to_markdown()

Reconstruct markdown with current checkbox states.

Returns:

Type Description
str

GitHub-flavored markdown string with checkbox syntax.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
def to_markdown(self) -> str:
    """Reconstruct markdown with current checkbox states.

    Returns:
        GitHub-flavored markdown string with checkbox syntax.
    """
    if not self._checkboxes:
        return self._text_edit.toMarkdown(MD_DIALECT)

    # Get current text and replace placeholders with checkbox syntax
    # we use toMarkdown() to make sure the other formatted items have
    # already been dealt with.
    text = self._text_edit.toMarkdown(MD_DIALECT)
    lines = text.split("\n")
    result_lines: List[str] = []

    checkbox_iter = iter(self._checkboxes)
    current_cb = next(checkbox_iter, None)

    for line in lines:
        if "\ufffc" in line and current_cb:
            checked_char = "x" if current_cb.checked else " "
            cb_idx = line.index("\ufffc")
            raw_after = line[cb_idx + 1:]       # everything after \ufffc
            raw_after = raw_after.lstrip(" ")    # drop the space Qt inserts
            raw_after = _PH_RE.sub("", raw_after).rstrip()
            line = f"{current_cb.prefix}[{checked_char}] {raw_after}"
            current_cb = next(checkbox_iter, None)
        result_lines.append(line)

    return "\n".join(result_lines)

toggle_checkbox(index)

Toggle a checkbox by its index.

Parameters:

Name Type Description Default
index int

The index of the checkbox to toggle.

required

Returns:

Type Description
bool

True if the checkbox was toggled, False otherwise.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def toggle_checkbox(self, index: int) -> bool:
    """Toggle a checkbox by its index.

    Args:
        index: The index of the checkbox to toggle.

    Returns:
        True if the checkbox was toggled, False otherwise.
    """
    if not 0 <= index < len(self._checkboxes):
        return False

    # Toggle the state
    self._checkboxes[index].checked = not self._checkboxes[index].checked

    # Update the document
    self._update_checkbox_in_document(index)

    # Emit signal
    self.checklist_changed.emit()
    return True

CheckboxItem dataclass

Represents a checkbox item in the document.

Attributes:

Name Type Description
index int

Unique index of the checkbox.

checked bool

Whether the checkbox is checked.

text str

Text following the checkbox.

line_number int

Line number in the original markdown.

prefix str

Characters before the checkbox (e.g., "- ", "* ").

doc_position int

Character position of the \ufffc object in the document, or -1 if not yet placed.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@dataclass
class CheckboxItem:
    """Represents a checkbox item in the document.

    Attributes:
        index: Unique index of the checkbox.
        checked: Whether the checkbox is checked.
        text: Text following the checkbox.
        line_number: Line number in the original markdown.
        prefix: Characters before the checkbox (e.g., "- ", "* ").
        doc_position: Character position of the \\ufffc object in the
            document, or -1 if not yet placed.
    """

    index: int
    checked: bool
    text: str
    line_number: int
    prefix: str = "- "
    doc_position: int = field(default=-1)

CheckboxTextObject

Bases: QPyTextObject

Custom text object for rendering checkbox icons in QTextDocument.

Uses Material icons to render checkboxes: - Unchecked: white radio_button_unchecked - Checked: green check_circle

Source code in client/ayon_ui_qt/components/checkbox_handler.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class CheckboxTextObject(QPyTextObject):
    """Custom text object for rendering checkbox icons in QTextDocument.

    Uses Material icons to render checkboxes:
    - Unchecked: white radio_button_unchecked
    - Checked: green check_circle
    """

    def __init__(self, parent: QObject | None = None):
        super().__init__(parent)
        self._icon_cache: dict[bool, QPixmap] = {}

    def intrinsicSize(
        self,
        doc: QTextDocument,
        posInDocument: int,
        format: QTextFormat,
    ) -> QSizeF:
        """Return the size of the checkbox icon.

        Args:
            doc: The document containing the object.
            posInDocument: Position in the document.
            format: The text format of the object.

        Returns:
            Size of the checkbox icon based on font size.
        """
        font = doc.defaultFont()
        size = max(font.pointSizeF() * 1.4, 14)
        return QSizeF(size, size)

    def drawObject(
        self,
        painter: QPainter,
        rect: Union[QRectF, QRect],
        doc: QTextDocument,
        posInDocument: int,
        format: QTextFormat,
    ) -> None:
        """Draw the checkbox icon.

        Args:
            painter: The painter to draw with.
            rect: The rectangle to draw in.
            doc: The document containing the object.
            posInDocument: Position in the document.
            format: The text format containing checkbox state.
        """
        is_checked = format.property(CHECKBOX_CHECKED_PROP)

        if is_checked:
            icon = get_icon(CHECKED_ICON, color=CHECKED_COLOR)
        else:
            icon = get_icon(UNCHECKED_ICON, color=UNCHECKED_COLOR)

        size = int(min(rect.width(), rect.height()))
        pixmap = icon.pixmap(size, size)

        # Vertically center the icon relative to text ascent
        font_metrics = QFontMetrics(doc.defaultFont())
        ascent = font_metrics.ascent()
        y_offset = (ascent - size) // 3
        draw_point = rect.topLeft().toPoint() + QPoint(0, -y_offset)

        painter.drawPixmap(draw_point, pixmap)

drawObject(painter, rect, doc, posInDocument, format)

Draw the checkbox icon.

Parameters:

Name Type Description Default
painter QPainter

The painter to draw with.

required
rect Union[QRectF, QRect]

The rectangle to draw in.

required
doc QTextDocument

The document containing the object.

required
posInDocument int

Position in the document.

required
format QTextFormat

The text format containing checkbox state.

required
Source code in client/ayon_ui_qt/components/checkbox_handler.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def drawObject(
    self,
    painter: QPainter,
    rect: Union[QRectF, QRect],
    doc: QTextDocument,
    posInDocument: int,
    format: QTextFormat,
) -> None:
    """Draw the checkbox icon.

    Args:
        painter: The painter to draw with.
        rect: The rectangle to draw in.
        doc: The document containing the object.
        posInDocument: Position in the document.
        format: The text format containing checkbox state.
    """
    is_checked = format.property(CHECKBOX_CHECKED_PROP)

    if is_checked:
        icon = get_icon(CHECKED_ICON, color=CHECKED_COLOR)
    else:
        icon = get_icon(UNCHECKED_ICON, color=UNCHECKED_COLOR)

    size = int(min(rect.width(), rect.height()))
    pixmap = icon.pixmap(size, size)

    # Vertically center the icon relative to text ascent
    font_metrics = QFontMetrics(doc.defaultFont())
    ascent = font_metrics.ascent()
    y_offset = (ascent - size) // 3
    draw_point = rect.topLeft().toPoint() + QPoint(0, -y_offset)

    painter.drawPixmap(draw_point, pixmap)

intrinsicSize(doc, posInDocument, format)

Return the size of the checkbox icon.

Parameters:

Name Type Description Default
doc QTextDocument

The document containing the object.

required
posInDocument int

Position in the document.

required
format QTextFormat

The text format of the object.

required

Returns:

Type Description
QSizeF

Size of the checkbox icon based on font size.

Source code in client/ayon_ui_qt/components/checkbox_handler.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def intrinsicSize(
    self,
    doc: QTextDocument,
    posInDocument: int,
    format: QTextFormat,
) -> QSizeF:
    """Return the size of the checkbox icon.

    Args:
        doc: The document containing the object.
        posInDocument: Position in the document.
        format: The text format of the object.

    Returns:
        Size of the checkbox icon based on font size.
    """
    font = doc.defaultFont()
    size = max(font.pointSizeF() * 1.4, 14)
    return QSizeF(size, size)