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
21// #define DEBUG_PAINTING
22using namespace TextAutogenerateText;
23TextAutogenerateListViewDelegate::TextAutogenerateListViewDelegate(QListView *view)
24 : QItemDelegate{view}
25 , mListView(view)
26 , mTextSelection(new TextAutogenerateListViewTextSelection(this, this))
27{
28 mSizeHintCache.setMaxEntries(32);
29 connect(mTextSelection, &TextAutogenerateListViewTextSelection::repaintNeeded, this, &TextAutogenerateListViewDelegate::updateView);
30}
31
32TextAutogenerateListViewDelegate::~TextAutogenerateListViewDelegate() = default;
33
34void TextAutogenerateListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
35{
36 painter->save();
37 drawBackground(painter, option, index);
38 painter->restore();
39
40 const MessageLayout layout = doLayout(option, index);
41 if (layout.textRect.isValid()) {
42#ifdef DEBUG_PAINTING
43 painter->save();
44 painter->setPen(QPen(Qt::red));
45 painter->drawRect(layout.textRect);
46 painter->restore();
47#endif
48 draw(painter, layout.textRect, index, option);
49 }
50}
51
52void TextAutogenerateListViewDelegate::draw(QPainter *painter, QRect rect, const QModelIndex &index, const QStyleOptionViewItem &option) const
53{
54 auto *doc = documentForIndex(index, rect.width());
55 if (!doc) {
56 return;
57 }
58 painter->save();
59 painter->translate(rect.left(), rect.top());
60 const QRect clip(0, 0, rect.width(), rect.height());
61
62 QAbstractTextDocumentLayout::PaintContext ctx;
63 if (mTextSelection) {
64 const QList<QAbstractTextDocumentLayout::Selection> selections = TextAutogenerateDelegateUtils::selection(mTextSelection, doc, index, option);
65 // Same as pDoc->drawContents(painter, clip) but we also set selections
66 ctx.selections = selections;
67 if (clip.isValid()) {
68 painter->setClipRect(clip);
69 ctx.clip = clip;
70 }
71 }
72 doc->documentLayout()->draw(painter, ctx);
73 painter->restore();
74}
75
76QSize TextAutogenerateListViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
77{
78 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
79 auto it = mSizeHintCache.find(uuid);
80 if (it != mSizeHintCache.end()) {
81 const QSize result = it->value;
82 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "TextAutogenerateListViewDelegate: SizeHint found in cache: " << result;
83 return result;
84 }
85
86 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
87 const QSize size = {layout.textRect.width(), layout.textRect.height()};
88 if (!size.isEmpty()) {
89 mSizeHintCache.insert(uuid, size);
90 }
91 return size;
92}
93
94void TextAutogenerateListViewDelegate::clearCache()
95{
96 mSizeHintCache.clear();
97 mDocumentCache.clear();
98}
99
100void TextAutogenerateListViewDelegate::clearSizeHintCache()
101{
102 mSizeHintCache.clear();
103}
104
105void TextAutogenerateListViewDelegate::removeMessageCache(const QByteArray &uuid)
106{
107 mDocumentCache.remove(uuid);
108 mSizeHintCache.remove(uuid);
109}
110
111TextAutogenerateListViewDelegate::MessageLayout TextAutogenerateListViewDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
112{
113 TextAutogenerateListViewDelegate::MessageLayout layout;
114 QRect usableRect = option.rect;
115 const TextAutoGenerateMessage::Sender sender = index.data(TextAutoGenerateChatModel::SenderRole).value<TextAutoGenerateMessage::Sender>();
116 const int indent = (sender == TextAutoGenerateMessage::Sender::User) ? 80 : 30;
117 const int maxWidth = qMax(30, option.rect.width() - 2 * indent);
118 const QSize textSize = sizeHint(index, maxWidth, option, &layout.baseLine);
119 layout.textRect = QRect(indent, usableRect.top(), maxWidth, textSize.height());
120 return layout;
121}
122
123QSize TextAutogenerateListViewDelegate::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const
124{
125 Q_UNUSED(option)
126 auto *doc = documentForIndex(index, maxWidth);
127 return textSizeHint(doc, pBaseLine);
128}
129
130QSize TextAutogenerateListViewDelegate::textSizeHint(QTextDocument *doc, qreal *pBaseLine) const
131{
132 if (!doc) {
133 return {};
134 }
135 const QSize size(doc->idealWidth(), doc->size().height()); // do the layouting, required by lineAt(0) below
136
137 const QTextLine &line = doc->firstBlock().layout()->lineAt(0);
138 *pBaseLine = line.y() + line.ascent(); // relative
139 // qDebug() << " doc->" << doc->toPlainText() << " size " << size;
140 return size;
141}
142
143void TextAutogenerateListViewDelegate::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index)
144{
145 Q_UNUSED(option);
146 mTextSelection->selectMessage(index);
147 mListView->update(index);
148 TextAutogenerateDelegateUtils::setClipboardSelection(mTextSelection);
149}
150
151bool TextAutogenerateListViewDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
152{
153 const QEvent::Type eventType = event->type();
154 if (eventType == QEvent::MouseButtonRelease) {
155 auto mev = static_cast<QMouseEvent *>(event);
156 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
157 if (handleMouseEvent(mev, layout.textRect, option, index)) {
158 return true;
159 }
160 } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) {
161 auto mev = static_cast<QMouseEvent *>(event);
162 if (mev->buttons() & Qt::LeftButton) {
163 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
164 if (handleMouseEvent(mev, layout.textRect, option, index)) {
165 return true;
166 }
167 }
168 }
169 return false;
170}
171
172bool TextAutogenerateListViewDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
173{
174 if (!index.isValid()) {
175 return false;
176 }
177 if (helpEvent->type() == QEvent::ToolTip) {
178 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
179 const QPoint helpEventPos{helpEvent->pos()};
180 if (layout.textRect.contains(helpEventPos)) {
181 const auto *doc = documentForIndex(index, layout.textRect.width());
182 if (!doc) {
183 return false;
184 }
185
186 const QPoint pos = helpEvent->pos() - layout.textRect.topLeft();
187 QString formattedTooltip;
188 if (TextAutogenerateDelegateUtils::generateToolTip(doc, pos, formattedTooltip)) {
189 QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view);
190 return true;
191 }
192 return true;
193 }
194 }
195 return false;
196}
197
198QTextDocument *TextAutogenerateListViewDelegate::documentForIndex(const QModelIndex &index, int width) const
199{
200 Q_ASSERT(index.isValid());
201 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
202 Q_ASSERT(!uuid.isEmpty());
203 auto it = mDocumentCache.find(uuid);
204 if (it != mDocumentCache.end()) {
205 auto ret = it->value.get();
206 if (width != -1 && !qFuzzyCompare(ret->textWidth(), width)) {
207 ret->setTextWidth(width);
208 }
209 return ret;
210 }
211
212 const QString text = index.data(TextAutoGenerateChatModel::MessageRole).toString();
213 if (text.isEmpty()) {
214 return nullptr;
215 }
216 auto doc = createTextDocument(text, width);
217 auto ret = doc.get();
218 mDocumentCache.insert(uuid, std::move(doc));
219 return ret;
220}
221
222std::unique_ptr<QTextDocument> TextAutogenerateListViewDelegate::createTextDocument(const QString &text, int width) const
223{
224 std::unique_ptr<QTextDocument> doc(new QTextDocument);
225 doc->setHtml(text);
226 doc->setTextWidth(width);
227 QTextFrame *frame = doc->frameAt(0);
228 QTextFrameFormat frameFormat = frame->frameFormat();
229 frameFormat.setMargin(0);
230 frame->setFrameFormat(frameFormat);
231 return doc;
232}
233QString TextAutogenerateListViewDelegate::selectedText() const
234{
235 return mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Text);
236}
237
238bool TextAutogenerateListViewDelegate::hasSelection() const
239{
240 return mTextSelection->hasSelection();
241}
242
243bool TextAutogenerateListViewDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
244{
245 const TextAutogenerateListViewDelegate::MessageLayout layout = doLayout(option, index);
246 if (maybeStartDrag(event, layout.textRect, option, index)) {
247 return true;
248 }
249 return false;
250}
251
252bool TextAutogenerateListViewDelegate::maybeStartDrag(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
253{
254 if (!mTextSelection->mightStartDrag()) {
255 return false;
256 }
257 if (mTextSelection->hasSelection()) {
258 const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
259 const auto *doc = documentForIndex(index, messageRect.width());
260 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
261 if (charPos != -1 && mTextSelection->contains(index, charPos)) {
262 auto mimeData = new QMimeData;
263 mimeData->setHtml(mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Html));
264 mimeData->setText(mTextSelection->selectedText(TextAutogenerateListViewTextSelection::Format::Text));
265 auto drag = new QDrag(const_cast<QWidget *>(option.widget));
266 drag->setMimeData(mimeData);
267 drag->exec(Qt::CopyAction);
268 mTextSelection->setMightStartDrag(false); // don't clear selection on release
269 return true;
270 }
271 }
272 return false;
273}
274bool TextAutogenerateListViewDelegate::handleMouseEvent(QMouseEvent *mouseEvent,
275 QRect messageRect,
276 const QStyleOptionViewItem &option,
277 const QModelIndex &index)
278{
279 Q_UNUSED(option)
280 if (!messageRect.contains(mouseEvent->pos())) {
281 return false;
282 }
283
284 const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
285 const QEvent::Type eventType = mouseEvent->type();
286
287 // Text selection
288 switch (eventType) {
290 mTextSelection->setMightStartDrag(false);
291 if (const auto *doc = documentForIndex(index, messageRect.width())) {
292 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
293 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "pressed at pos" << charPos;
294 if (charPos == -1) {
295 return false;
296 }
297 if (mTextSelection->contains(index, charPos) && doc->documentLayout()->hitTest(pos, Qt::ExactHit) != -1) {
298 mTextSelection->setMightStartDrag(true);
299 return true;
300 }
301
302 // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection
303 // (look there if you want to add these things)
304
305 mTextSelection->setTextSelectionStart(index, charPos);
306 return true;
307 } else {
308 mTextSelection->clear();
309 }
310 break;
312 if (!mTextSelection->mightStartDrag()) {
313 if (const auto *doc = documentForIndex(index, messageRect.width())) {
314 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
315 if (charPos != -1) {
316 // QWidgetTextControl also has code to support isPreediting()/commitPreedit(), selectBlockOnTripleClick
317 mTextSelection->setTextSelectionEnd(index, charPos);
318 return true;
319 }
320 }
321 }
322 break;
324 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "released";
325 TextAutogenerateDelegateUtils::setClipboardSelection(mTextSelection);
326 // Clicks on links
327 if (!mTextSelection->hasSelection()) {
328 if (const auto *doc = documentForIndex(index, messageRect.width())) {
329 const QString link = doc->documentLayout()->anchorAt(pos);
330 if (!link.isEmpty()) {
331 QDesktopServices::openUrl(QUrl(link));
332 return true;
333 }
334 }
335 } else if (mTextSelection->mightStartDrag()) {
336 // clicked into selection, didn't start drag, clear it (like kwrite and QTextEdit)
337 mTextSelection->clear();
338 }
339 // don't return true here, we need to send mouse release events to other helpers (ex: click on image)
340 break;
342 if (!mTextSelection->hasSelection()) {
343 if (const auto *doc = documentForIndex(index, messageRect.width())) {
344 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
345 qCDebug(TEXTAUTOGENERATETEXT_WIDGET_LOG) << "double-clicked at pos" << charPos;
346 if (charPos == -1) {
347 return false;
348 }
349 mTextSelection->selectWordUnderCursor(index, charPos);
350 return true;
351 }
352 }
353 break;
354 default:
355 break;
356 }
357
358 return false;
359}
360
361#include "moc_textautogeneratelistviewdelegate.cpp"
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
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
virtual bool event(QEvent *e)
QObject * sender() const const
void drawRect(const QRect &rectangle)
void restore()
void save()
void setClipRect(const QRect &rectangle, Qt::ClipOperation operation)
void setPen(Qt::PenStyle style)
void translate(const QPoint &offset)
bool contains(const QPoint &point, bool proper) const const
int height() const const
int left() const const
int top() const const
QPoint topLeft() const const
int width() const const
int height() const const
bool isEmpty() const const
bool isEmpty() const const
CopyAction
FuzzyHit
LeftButton
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 18 2025 12:00:52 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.