# -*- coding: utf-8 -*-
"""
:File: EuljiroWorship/controller/utils/keyword_highlight_delegate.py
:Author: Benjamin Jaedon Choi - https://github.com/saintbenjamin
:Affiliated Church: The Eulji-ro Presbyterian Church [대한예수교장로회(통합) 을지로교회]
:Address: The Eulji-ro Presbyterian Church, 24-10, Eulji-ro 20-gil, Jung-gu, Seoul 04549, South Korea
:Telephone: +82-2-2266-3070
:E-mail: euljirochurch [at] G.M.A.I.L. (replace [at] with @ and G.M.A.I.L as you understood.)
:License: MIT License with Attribution Requirement (see LICENSE file for details); Copyright (c) 2025 The Eulji-ro Presbyterian Church.
`QStyledItemDelegate <https://doc.qt.io/qt-6/qstyleditemdelegate.html>`_ implementation for keyword highlighting in `Qt <https://doc.qt.io/qt-6/index.html>`_ item views.
This module defines a delegate that renders cell text using `QTextDocument <https://doc.qt.io/qt-6/qtextdocument.html>`_ so that:
- HTML tags (e.g., ``<b>``) are supported.
- Newlines are rendered as multi-line content (``<br>``).
- Specified keywords are highlighted by wrapping them in red ``<span>`` tags.
- Row height can be computed from the HTML-rendered content.
Typical usage::
view.setItemDelegate(KeywordHighlightDelegate(keywords=["하나님", "세상"]))
"""
from PySide6.QtWidgets import QStyledItemDelegate, QStyle
from PySide6.QtGui import QTextDocument
from PySide6.QtCore import QPoint, QSize
[docs]
class KeywordHighlightDelegate(QStyledItemDelegate):
"""
Custom item delegate that highlights keywords in model text using HTML rendering.
This delegate renders each cell via ``QTextDocument`` so it can support:
- HTML formatting (keyword highlighting)
- Multi-line wrapping
- Accurate :meth:`sizeHint` based on rendered document height
Attributes:
keywords (list[str]):
Keywords to highlight. Empty strings are ignored. Matching is performed
via simple substring replacement after HTML-escaping the cell text.
"""
[docs]
def __init__(self, keywords, parent=None):
"""
Initialize the delegate with keywords to highlight.
Args:
keywords (list[str]):
Keywords to highlight. Empty strings are ignored.
parent (QWidget | None):
Optional parent widget.
Returns:
None
"""
super().__init__(parent)
self.keywords = keywords
[docs]
def paint(self, painter, option, index):
"""
Paint a single cell using HTML rendering, highlighting configured keywords.
Behavior:
- Fills the background depending on selection state.
- Converts the cell text to HTML with keyword highlighting.
- Uses `QTextDocument <https://doc.qt.io/qt-6/qtextdocument.html>`_ to render the HTML with wrapping.
- Draws the document contents with a small padding offset.
Args:
painter (QPainter):
Painter used for drawing.
option (QStyleOptionViewItem):
Style and geometry information for the item.
index (QModelIndex):
Model index for the cell being painted.
Returns:
None
"""
text = index.data()
if not text:
return super().paint(painter, option, index)
painter.save()
# Draw background depending on selection state
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
else:
painter.fillRect(option.rect, option.backgroundBrush)
# Convert keywords to HTML-marked-up string
html = self._highlight_keywords(text)
# Use QTextDocument for proper HTML rendering and line wrapping
doc = QTextDocument()
doc.setDefaultFont(option.font)
doc.setHtml(html)
doc.setTextWidth(option.rect.width() - 8) # Account for horizontal padding
# Offset drawing to avoid touching borders
painter.translate(option.rect.topLeft() + QPoint(4, 4))
doc.drawContents(painter)
painter.restore()
[docs]
def _highlight_keywords(self, text):
"""
Convert raw text into HTML with keyword highlighting.
This method:
- Escapes HTML special characters (``&``, ``<``, ``>``).
- Converts newline characters to ``<br>``.
- Wraps each keyword occurrence with::
<span style='color:red'>...</span>
Note:
This implementation performs simple string replacement and does not use regex or word-boundary matching.
Args:
text (str):
Original cell text.
Returns:
str:
HTML-formatted string with highlighted keywords.
"""
escaped_text = (
text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\n", "<br>")
)
for kw in self.keywords:
if not kw:
continue
escaped_kw = kw.replace("&", "&").replace("<", "<").replace(">", ">")
escaped_text = escaped_text.replace(
escaped_kw, f"<span style='color:red'>{escaped_kw}</span>"
)
return escaped_text
[docs]
def sizeHint(self, option, index):
"""
Return the preferred size for a cell based on HTML-rendered content.
Computes the document size using `QTextDocument <https://doc.qt.io/qt-6/qtextdocument.html>`_ with the same width constraints
used during painting, then adds padding so the content does not touch borders.
Args:
option (QStyleOptionViewItem):
Style and geometry information for the item.
index (QModelIndex):
Model index for the cell.
Returns:
QSize:
Preferred size for the rendered cell content.
"""
text = index.data()
if not text:
return super().sizeHint(option, index)
html = self._highlight_keywords(text)
doc = QTextDocument()
doc.setDefaultFont(option.font)
doc.setHtml(html)
doc.setTextWidth(option.rect.width() - 8)
return doc.size().toSize() + QSize(8, 8) # padding