KTextAddons

textautogeneratelistviewdelegate.cpp
1/*
2 SPDX-FileCopyrightText: 2025 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6#include "textautogeneratelistviewdelegate.h"
7#include "core/textautogeneratechatmodel.h"
8#include "textautogeneratedelegateutils.h"
9#include "textautogeneratelistviewtextselection.h"
10#include "textautogeneratetextwidget_debug.h"
11#include <QAbstractTextDocumentLayout>
12#include <QDesktopServices>
13#include <QDrag>
14#include <QListView>
15#include <QMimeData>
16#include <QPainter>
17#include <QTextFrame>
18#include <QTextFrameFormat>
19#include <QToolTip>
20
21using namespace TextAutogenerateText;
22TextAutogenerateListViewDelegate::TextAutogenerateListViewDelegate(QListView *view)
23 : QItemDelegate{view}
24 , mListView(view)
25 , mTextSelection(new TextAutogenerateListViewTextSelection(this, this))
26{
27 mSizeHintCache.setMaxEntries(32);
28 connect(mTextSelection, &TextAutogenerateListViewTextSelection::repaintNeeded, this, &TextAutogenerateListViewDelegate::updateView);
29}
30
31TextAutogenerateListViewDelegate::~TextAutogenerateListViewDelegate() = default;
32
33void TextAutogenerateListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
34{
35 painter->save();
36 drawBackground(painter, option, index);
37 painter->restore();
38
39 const MessageLayout layout = doLayout(option, index);
40 if (layout.textRect.isValid()) {
41 painter->save();
42 painter->setPen(QPen(Qt::red));
44 painter->drawRoundedRect(
45 QRect(layout.decoRect.topLeft(), QSize(layout.decoRect.width(), layout.decoRect.height() - TextAutogenerateDelegateUtils::spacingText() - 5)),
46 TextAutogenerateDelegateUtils::roundRectValue(),
47 TextAutogenerateDelegateUtils::roundRectValue());
48 painter->restore();
49 draw(painter, layout.textRect, index, option);
50 }
51 /*
52 painter->save();
53 painter->setPen(QPen(Qt::green));
54 painter->drawRect(layout.decoRect);
55 painter->restore();
56 */
57}
58
59void TextAutogenerateListViewDelegate::draw(QPainter *painter, QRect rect, const QModelIndex &index, const QStyleOptionViewItem &option) const
60{
61 auto *doc = documentForIndex(index, rect.width());
62 if (!doc) {
63 return;
64 }
65 painter->save();
66 painter->translate(rect.left(), rect.top());
67 const QRect clip(0, 0, rect.width(), rect.height());
68
69 QAbstractTextDocumentLayout::PaintContext ctx;
70 if (mTextSelection) {
71 const QList<QAbstractTextDocumentLayout::Selection> selections = TextAutogenerateDelegateUtils::selection(mTextSelection, doc, index, option);
72 // Same as pDoc->drawContents(painter, clip) but we also set selections
73 ctx.selections = selections;
74 if (clip.isValid()) {
75 painter->setClipRect(clip);
76 ctx.clip = clip;
77 }
78 }
79 doc->documentLayout()->draw(painter, ctx);
80 painter->restore();
81 drawDate(painter, index, option);
82}
83
84void TextAutogenerateListViewDelegate::drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option) const
85{
86 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
87 if (!layout.dateSize.isValid()) {
88 return;
89 }
90
91 const QPen origPen = painter->pen();
92 const qreal margin = TextAutogenerateDelegateUtils::leftLLMIndent();
93 const QString dateStr = index.data(TextAutoGenerateChatModel::DateTimeStrRole).toString();
94 const QRect dateAreaRect(layout.decoRect.x(),
95 layout.textRect.y() + layout.textRect.height() + TextAutogenerateDelegateUtils::spacingText(),
96 layout.textRect.width(),
97 layout.dateSize.height()); // the whole row
98
99 /*
100 // qDebug() << " draw date" << dateAreaRect << layout.decoRect;
101 painter->save();
102 painter->setPen(QPen(Qt::yellow));
103 painter->drawRect(dateAreaRect);
104 painter->restore();
105 */
106
107 const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, layout.dateSize, dateAreaRect);
108 QColor lightColor(painter->pen().color());
109 lightColor.setAlpha(60);
110 painter->setPen(lightColor);
111 painter->drawText(dateTextRect, dateStr);
112 // qDebug() << " dateTextRect" << dateTextRect;
113 const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2;
114 painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY);
115 painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY);
116 painter->setPen(origPen);
117}
118
119QSize TextAutogenerateListViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
120{
121 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
122 auto it = mSizeHintCache.find(uuid);
123 if (it != mSizeHintCache.end()) {
124 const QSize result = it->value;
125 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "TextAutogenerateListViewDelegate: SizeHint found in cache: " << result;
126 return result;
127 }
128
129 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
130
131 int additionalHeight = 0;
132 // A little bit of margin below the very last item, it just looks better
133 if (index.row() == index.model()->rowCount() - 1) {
134 additionalHeight += 4;
135 }
136
137 const QSize size = {layout.decoRect.width(), layout.decoRect.height() + additionalHeight + layout.dateSize.height()};
138 if (!size.isEmpty()) {
139 mSizeHintCache.insert(uuid, size);
140 }
141 return size;
142}
143
144void TextAutogenerateListViewDelegate::clearCache()
145{
146 clearSizeHintCache();
147 mDocumentCache.clear();
148}
149
150void TextAutogenerateListViewDelegate::clearSizeHintCache()
151{
152 mSizeHintCache.clear();
153}
154
155void TextAutogenerateListViewDelegate::removeMessageCache(const QByteArray &uuid)
156{
157 mDocumentCache.remove(uuid);
158 mSizeHintCache.remove(uuid);
159}
160
161TextAutogenerateListViewDelegate::MessageLayout TextAutogenerateListViewDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
162{
163 TextAutogenerateListViewDelegate::MessageLayout layout;
164 QRect usableRect = option.rect;
165 if (index.data(TextAutoGenerateChatModel::SenderRole).value<TextAutoGenerateMessage::Sender>() == TextAutoGenerateMessage::Sender::LLM) {
166 const QString dateStr = index.data(TextAutoGenerateChatModel::DateTimeStrRole).toString();
167 layout.dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateStr);
168 usableRect.setBottom(usableRect.bottom() + layout.dateSize.height());
169 }
170 const TextAutoGenerateMessage::Sender sender = index.data(TextAutoGenerateChatModel::SenderRole).value<TextAutoGenerateMessage::Sender>();
171 const bool isUser = (sender == TextAutoGenerateMessage::Sender::User);
172 const int indent = isUser ? TextAutogenerateDelegateUtils::leftUserIndent() : TextAutogenerateDelegateUtils::leftLLMIndent();
173 const int maxWidth = qMax(30, option.rect.width() - indent - TextAutogenerateDelegateUtils::rightIndent());
174 const QSize textSize = sizeHint(index, maxWidth, option, &layout.baseLine);
175
176 layout.textRect = QRect(indent + TextAutogenerateDelegateUtils::marginText(),
177 usableRect.top() + TextAutogenerateDelegateUtils::spacingText() * 2,
178 maxWidth - TextAutogenerateDelegateUtils::marginText(),
179 textSize.height());
180
181 layout.decoRect = QRect(indent,
182 usableRect.top() + TextAutogenerateDelegateUtils::spacingText(),
183 maxWidth,
184 layout.textRect.height() + TextAutogenerateDelegateUtils::spacingText() * 3);
185
186 return layout;
187}
188
189QSize TextAutogenerateListViewDelegate::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const
190{
191 Q_UNUSED(option)
192 auto *doc = documentForIndex(index, maxWidth);
193 return textSizeHint(doc, pBaseLine);
194}
195
196QSize TextAutogenerateListViewDelegate::textSizeHint(QTextDocument *doc, qreal *pBaseLine) const
197{
198 if (!doc) {
199 return {};
200 }
201 const QSize size(doc->idealWidth(), doc->size().height()); // do the layouting, required by lineAt(0) below
202
203 const QTextLine &line = doc->firstBlock().layout()->lineAt(0);
204 *pBaseLine = line.y() + line.ascent(); // relative
205 // qDebug() << " doc->" << doc->toPlainText() << " size " << size;
206 return size;
207}
208
209void TextAutogenerateListViewDelegate::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index)
210{
211 Q_UNUSED(option);
212 mTextSelection->selectMessage(index);
213 mListView->update(index);
214 TextAutogenerateDelegateUtils::setClipboardSelection(mTextSelection);
215}
216
217bool TextAutogenerateListViewDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
218{
219 const QEvent::Type eventType = event->type();
220 if (eventType == QEvent::MouseButtonRelease) {
221 auto mev = static_cast<QMouseEvent *>(event);
222 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
223 if (handleMouseEvent(mev, layout.textRect, option, index)) {
224 return true;
225 }
226 } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) {
227 auto mev = static_cast<QMouseEvent *>(event);
228 if (mev->buttons() & Qt::LeftButton) {
229 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
230 if (handleMouseEvent(mev, layout.textRect, option, index)) {
231 return true;
232 }
233 }
234 }
235 return false;
236}
237
238bool TextAutogenerateListViewDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
239{
240 if (!index.isValid()) {
241 return false;
242 }
243 if (helpEvent->type() == QEvent::ToolTip) {
244 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
245 const QPoint helpEventPos{helpEvent->pos()};
246 if (layout.textRect.contains(helpEventPos)) {
247 const auto *doc = documentForIndex(index, layout.textRect.width());
248 if (!doc) {
249 return false;
250 }
251
252 const QPoint pos = helpEvent->pos() - layout.textRect.topLeft();
253 QString formattedTooltip;
254 if (TextAutogenerateDelegateUtils::generateToolTip(doc, pos, formattedTooltip)) {
255 QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view);
256 return true;
257 }
258 return true;
259 }
260 }
261 return false;
262}
263
264QTextDocument *TextAutogenerateListViewDelegate::documentForIndex(const QModelIndex &index, int width) const
265{
266 Q_ASSERT(index.isValid());
267 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
268 Q_ASSERT(!uuid.isEmpty());
269 auto it = mDocumentCache.find(uuid);
270 if (it != mDocumentCache.end()) {
271 auto ret = it->value.get();
272 if (width != -1 && !qFuzzyCompare(ret->textWidth(), width)) {
273 ret->setTextWidth(width);
274 }
275 return ret;
276 }
277
278 const QString text = index.data(TextAutoGenerateChatModel::MessageRole).toString();
279 if (text.isEmpty()) {
280 return nullptr;
281 }
282 auto doc = createTextDocument(text, width);
283 auto ret = doc.get();
284 mDocumentCache.insert(uuid, std::move(doc));
285 return ret;
286}
287
288std::unique_ptr<QTextDocument> TextAutogenerateListViewDelegate::createTextDocument(const QString &text, int width) const
289{
290 std::unique_ptr<QTextDocument> doc(new QTextDocument);
291 // doc->setMarkdown(text);
292 doc->setHtml(text);
293 doc->setTextWidth(width);
294 QTextFrame *frame = doc->frameAt(0);
295 QTextFrameFormat frameFormat = frame->frameFormat();
296 frameFormat.setMargin(0);
297 frame->setFrameFormat(frameFormat);
298 return doc;
299}
300QString TextAutogenerateListViewDelegate::selectedText() const
301{
302 return mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Text);
303}
304
305bool TextAutogenerateListViewDelegate::hasSelection() const
306{
307 return mTextSelection->hasSelection();
308}
309
310bool TextAutogenerateListViewDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
311{
312 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
313 if (maybeStartDrag(event, layout.textRect, option, index)) {
314 return true;
315 }
316 return false;
317}
318
319bool TextAutogenerateListViewDelegate::maybeStartDrag(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
320{
321 if (!mTextSelection->mightStartDrag()) {
322 return false;
323 }
324 if (mTextSelection->hasSelection()) {
325 const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
326 const auto *doc = documentForIndex(index, messageRect.width());
327 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
328 if (charPos != -1 && mTextSelection->contains(index, charPos)) {
329 auto mimeData = new QMimeData;
330 mimeData->setHtml(mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Html));
331 mimeData->setText(mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Text));
332 auto drag = new QDrag(const_cast<QWidget *>(option.widget));
333 drag->setMimeData(mimeData);
334 drag->exec(Qt::CopyAction);
335 mTextSelection->setMightStartDrag(false); // don't clear selection on release
336 return true;
337 }
338 }
339 return false;
340}
341bool TextAutogenerateListViewDelegate::handleMouseEvent(QMouseEvent *mouseEvent,
342 QRect messageRect,
343 const QStyleOptionViewItem &option,
344 const QModelIndex &index)
345{
346 Q_UNUSED(option)
347 if (!messageRect.contains(mouseEvent->pos())) {
348 return false;
349 }
350
351 const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
352 const QEvent::Type eventType = mouseEvent->type();
353
354 // Text selection
355 switch (eventType) {
357 mTextSelection->setMightStartDrag(false);
358 if (const auto *doc = documentForIndex(index, messageRect.width())) {
359 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
360 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "pressed at pos" << charPos;
361 if (charPos == -1) {
362 return false;
363 }
364 if (mTextSelection->contains(index, charPos) && doc->documentLayout()->hitTest(pos, Qt::ExactHit) != -1) {
365 mTextSelection->setMightStartDrag(true);
366 return true;
367 }
368
369 // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection
370 // (look there if you want to add these things)
371
372 mTextSelection->setTextSelectionStart(index, charPos);
373 return true;
374 } else {
375 mTextSelection->clear();
376 }
377 break;
379 if (!mTextSelection->mightStartDrag()) {
380 if (const auto *doc = documentForIndex(index, messageRect.width())) {
381 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
382 if (charPos != -1) {
383 // QWidgetTextControl also has code to support isPreediting()/commitPreedit(), selectBlockOnTripleClick
384 mTextSelection->setTextSelectionEnd(index, charPos);
385 return true;
386 }
387 }
388 }
389 break;
391 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "released";
392 TextAutogenerateDelegateUtils::setClipboardSelection(mTextSelection);
393 // Clicks on links
394 if (!mTextSelection->hasSelection()) {
395 if (const auto *doc = documentForIndex(index, messageRect.width())) {
396 const QString link = doc->documentLayout()->anchorAt(pos);
397 if (!link.isEmpty()) {
398 QDesktopServices::openUrl(QUrl(link));
399 return true;
400 }
401 }
402 } else if (mTextSelection->mightStartDrag()) {
403 // clicked into selection, didn't start drag, clear it (like kwrite and QTextEdit)
404 mTextSelection->clear();
405 }
406 // don't return true here, we need to send mouse release events to other helpers (ex: click on image)
407 break;
409 if (!mTextSelection->hasSelection()) {
410 if (const auto *doc = documentForIndex(index, messageRect.width())) {
411 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
412 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "double-clicked at pos" << charPos;
413 if (charPos == -1) {
414 return false;
415 }
416 mTextSelection->selectWordUnderCursor(index, charPos);
417 return true;
418 }
419 }
420 break;
421 default:
422 break;
423 }
424
425 return false;
426}
427
428#include "moc_textautogeneratelistviewdelegate.cpp"
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
virtual int rowCount(const QModelIndex &parent) const const=0
QString anchorAt(const QPointF &position) const const
virtual int hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const const=0
bool isEmpty() const const
bool openUrl(const QUrl &url)
void drawBackground(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const const
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
int row() const const
virtual bool event(QEvent *e)
QObject * sender() const const
void drawLine(const QLine &line)
void drawRoundedRect(const QRect &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
void drawText(const QPoint &position, const QString &text)
const QPen & pen() const const
void restore()
void save()
void setClipRect(const QRect &rectangle, Qt::ClipOperation operation)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
void translate(const QPoint &offset)
QColor color() const const
int bottom() const const
bool contains(const QPoint &point, bool proper) const const
int height() const const
int left() const const
int right() const const
void setBottom(int y)
int top() const const
QPoint topLeft() const const
int width() const const
int x() const const
int y() const const
int height() const const
bool isEmpty() const const
bool isValid() const const
bool isEmpty() const const
QRect alignedRect(Qt::LayoutDirection direction, Qt::Alignment alignment, const QSize &size, const QRect &rectangle)
AlignCenter
CopyAction
FuzzyHit
LayoutDirectionAuto
LeftButton
TextSingleLine
QTextLayout * layout() const const
QAbstractTextDocumentLayout * documentLayout() const const
QTextBlock firstBlock() const const
qreal idealWidth() const const
void setHtml(const QString &html)
void setTextWidth(qreal width)
QTextFrameFormat frameFormat() const const
void setFrameFormat(const QTextFrameFormat &format)
void setMargin(qreal margin)
QTextLine lineAt(int i) const const
qreal ascent() const const
qreal y() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
QByteArray toByteArray() const const
QString toString() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:06:13 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.