KTextAddons

textautogeneratelistview.cpp
1/*
2 SPDX-FileCopyrightText: 2025 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6#include "textautogeneratelistview.h"
7#include "core/textautogeneratechatmodel.h"
8#include "core/textautogeneratechatsortfilterproxymodel.h"
9#include "core/textautogeneratemanager.h"
10#include "textautogeneratelistviewdelegate.h"
11#include "textautogeneratemessagewaitingansweranimation.h"
12#include "textautogenerateselectedmessagebackgroundanimation.h"
13#include <KLocalizedString>
14#include <QApplication>
15#include <QClipboard>
16#include <QMenu>
17#include <QMouseEvent>
18#include <QScrollBar>
19
20using namespace TextAutogenerateText;
21TextAutogenerateListView::TextAutogenerateListView(QWidget *parent)
22 : QListView(parent)
23 , mDelegate(new TextAutogenerateListViewDelegate(this))
24{
25 setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); // nicer in case of huge messages
26 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
27 setWordWrap(true); // so that the delegate sizeHint is called again when the width changes
28 setItemDelegate(mDelegate);
29 setFocusPolicy(Qt::NoFocus);
30 scrollToBottom();
31 setMouseTracking(true);
32 TextAutogenerateManager::self()->loadHistory();
33#if 0
34 auto filterProxyModel = new TextAutoGenerateChatSortFilterProxyModel(this);
35 filterProxyModel->setSourceModel(TextAutogenerateManager::self()->textAutoGenerateChatModel());
36 setModel(filterProxyModel);
37#else
38 setModel(TextAutogenerateManager::self()->textAutoGenerateChatModel());
39#endif
40 connect(TextAutogenerateManager::self()->textAutoGenerateChatModel(), &TextAutoGenerateChatModel::conversationCleared, this, [this]() {
41 mDelegate->clearCache();
42 });
43
44 connect(mDelegate, &TextAutogenerateListViewDelegate::updateView, this, [this](const QModelIndex &index) {
45 update(index);
46 });
47
48 connect(mDelegate, &TextAutogenerateListViewDelegate::removeMessage, this, &TextAutogenerateListView::slotRemoveMessage);
49 connect(mDelegate, &TextAutogenerateListViewDelegate::editMessage, this, &TextAutogenerateListView::slotEditMessage);
50 connect(mDelegate, &TextAutogenerateListViewDelegate::copyMessage, this, &TextAutogenerateListView::slotCopyMessage);
51 connect(mDelegate, &TextAutogenerateListViewDelegate::cancelRequest, this, &TextAutogenerateListView::slotCancelRequest);
52
53 connect(TextAutogenerateManager::self()->textAutoGenerateChatModel(),
55 this,
56 &TextAutogenerateListView::checkIfAtBottom);
57 connect(TextAutogenerateManager::self()->textAutoGenerateChatModel(),
59 this,
60 &TextAutogenerateListView::checkIfAtBottom);
61 connect(TextAutogenerateManager::self()->textAutoGenerateChatModel(),
63 this,
64 &TextAutogenerateListView::checkIfAtBottom);
65
66 connect(TextAutogenerateManager::self()->textAutoGenerateChatModel(),
68 this,
69 [this](const QModelIndex &topLeft, const QModelIndex &, const QList<int> &roles) {
70 if (roles.contains(TextAutoGenerateChatModel::MessageRole) || roles.contains(TextAutoGenerateChatModel::FinishedRole)) {
71 const QByteArray uuid = topLeft.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
72 if (!uuid.isEmpty()) {
73 mDelegate->removeMessageCache(uuid);
74 }
75 if (roles.contains(TextAutoGenerateChatModel::FinishedRole)) {
76 const bool inProgress = !topLeft.data(TextAutoGenerateChatModel::FinishedRole).toBool();
77 if (inProgress) {
78 addWaitingAnswerAnimation(topLeft);
79 } else {
80 Q_EMIT waitingAnswerDone(topLeft);
81 }
82 }
83 }
84 });
85
86 // Connect to rangeChanged rather than rowsInserted/rowsRemoved/modelReset.
87 // This way it also catches the case of an item changing height (e.g. after async image loading)
88 connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &TextAutogenerateListView::maybeScrollToBottom);
89}
90
91TextAutogenerateListView::~TextAutogenerateListView()
92{
93 TextAutogenerateManager::self()->saveHistory();
94 // TextAutogenerateManager::self()->textAutoGenerateChatModel()->resetConversation();
95}
96
97void TextAutogenerateListView::slotEditMessage(const QModelIndex &index)
98{
99 auto model = const_cast<QAbstractItemModel *>(index.model());
100 model->setData(index, true, TextAutoGenerateChatModel::EditingRole);
101 Q_EMIT editMessage(index);
102}
103
104void TextAutogenerateListView::slotRemoveMessage(const QModelIndex &index)
105{
106 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
107 if (!uuid.isEmpty()) {
108 Q_EMIT cancelRequest(uuid);
109 // TODO disconnect
110 TextAutogenerateManager::self()->textAutoGenerateChatModel()->removeDiscussion(uuid);
111 }
112}
113
114void TextAutogenerateListView::slotCancelRequest(const QModelIndex &index)
115{
116 const QByteArray uuid = index.data(TextAutoGenerateChatModel::UuidRole).toByteArray();
117 if (!uuid.isEmpty()) {
118 if (TextAutogenerateManager::self()->textAutoGenerateChatModel()->cancelRequest(index)) {
119 Q_EMIT cancelRequest(uuid);
120 }
121 }
122}
123
124void TextAutogenerateListView::slotCopyMessage(const QModelIndex &index)
125{
126 const QString currentValue = index.data().toString();
127 QClipboard *clip = QApplication::clipboard();
128 clip->setText(currentValue, QClipboard::Clipboard);
129 clip->setText(currentValue, QClipboard::Selection);
130}
131
132void TextAutogenerateListView::setMessages(const QList<TextAutoGenerateMessage> &msg)
133{
134 TextAutogenerateManager::self()->textAutoGenerateChatModel()->setMessages(msg);
135}
136
137void TextAutogenerateListView::contextMenuEvent(QContextMenuEvent *event)
138{
139 QMenu menu(this);
140 const QModelIndex index = indexAt(event->pos());
141 if (index.isValid()) {
142 auto copyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")),
143 mDelegate->hasSelection() ? i18nc("@action", "Copy Selection") : i18nc("@action", "Copy"),
144 &menu);
145 copyAction->setShortcut(QKeySequence::Copy);
146 connect(copyAction, &QAction::triggered, this, [index, this]() {
147 slotCopyMessage(index);
148 });
149 menu.addAction(copyAction);
150 menu.addSeparator();
151 auto selectAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-all")), i18nc("@action", "Select All"), &menu);
152 connect(selectAllAction, &QAction::triggered, this, [this, index]() {
153 slotSelectAll(index);
154 });
155 selectAllAction->setShortcut(QKeySequence::SelectAll);
156 menu.addAction(selectAllAction);
157 }
158 if (!menu.actions().isEmpty()) {
159 menu.exec(event->globalPos());
160 }
161}
162
163void TextAutogenerateListView::slotSelectAll(const QModelIndex &index)
164{
165 mDelegate->selectAll(listViewOptions(), index);
166}
167
168void TextAutogenerateListView::resizeEvent(QResizeEvent *ev)
169{
171
172 // Fix not being really at bottom when the view gets reduced by the header widget becoming taller
173 checkIfAtBottom();
174 maybeScrollToBottom(); // this forces a layout in QAIV, which then changes the vbar max value
175 updateVerticalPageStep();
176 mDelegate->clearSizeHintCache();
177}
178
179void TextAutogenerateListView::checkIfAtBottom()
180{
181 auto *vbar = verticalScrollBar();
182 mAtBottom = vbar->value() == vbar->maximum();
183}
184
185void TextAutogenerateListView::maybeScrollToBottom()
186{
187 if (mAtBottom) {
189 }
190}
191
192void TextAutogenerateListView::updateVerticalPageStep()
193{
195}
196
197void TextAutogenerateListView::mouseReleaseEvent(QMouseEvent *event)
198{
199 handleMouseEvent(event);
200}
201
202void TextAutogenerateListView::mouseDoubleClickEvent(QMouseEvent *event)
203{
204 handleMouseEvent(event);
205}
206
207void TextAutogenerateListView::mousePressEvent(QMouseEvent *event)
208{
209 mPressedPosition = event->pos();
210 handleMouseEvent(event);
211}
212
213void TextAutogenerateListView::mouseMoveEvent(QMouseEvent *event)
214{
215 // Drag support
216 const int distance = (event->pos() - mPressedPosition).manhattanLength();
217 if (distance > QApplication::startDragDistance()) {
218 mPressedPosition = {};
219 const QPersistentModelIndex index = indexAt(event->pos());
220 if (index.isValid()) {
221 QStyleOptionViewItem options = listViewOptions();
222 options.rect = visualRect(index);
223 if (maybeStartDrag(event, options, index)) {
224 return;
225 }
226 }
227 }
228 handleMouseEvent(event);
229}
230
231QStyleOptionViewItem TextAutogenerateListView::listViewOptions() const
232{
233 QStyleOptionViewItem option;
234 initViewItemOption(&option);
235 return option;
236}
237
238void TextAutogenerateListView::handleMouseEvent(QMouseEvent *event)
239{
240 const QPersistentModelIndex index = indexAt(event->pos());
241 if (index.isValid()) {
242 // When the cursor hovers another message, hide/show the reaction icon accordingly
243 if (mCurrentIndex != index) {
244 if (mCurrentIndex.isValid()) {
245 auto lastModel = const_cast<QAbstractItemModel *>(mCurrentIndex.model());
246 lastModel->setData(mCurrentIndex, false, TextAutoGenerateChatModel::MouseHoverRole);
247 }
248 mCurrentIndex = index;
249 auto model = const_cast<QAbstractItemModel *>(mCurrentIndex.model());
250 model->setData(mCurrentIndex, true, TextAutoGenerateChatModel::MouseHoverRole);
251 }
252 QStyleOptionViewItem options = listViewOptions();
253 options.rect = visualRect(mCurrentIndex);
254 if (mouseEvent(event, options, mCurrentIndex)) {
255 update(mCurrentIndex);
256 }
257 }
258}
259
260bool TextAutogenerateListView::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
261{
262 return mDelegate->maybeStartDrag(event, option, index);
263}
264
265bool TextAutogenerateListView::mouseEvent(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
266{
267 return mDelegate->mouseEvent(event, option, index);
268}
269
270void TextAutogenerateListView::leaveEvent(QEvent *event)
271{
272 if (mCurrentIndex.isValid()) {
273 auto lastModel = const_cast<QAbstractItemModel *>(mCurrentIndex.model());
274 lastModel->setData(mCurrentIndex, false, TextAutoGenerateChatModel::MouseHoverRole);
275 mCurrentIndex = QPersistentModelIndex();
276 }
278}
279
280void TextAutogenerateListView::handleKeyPressEvent(QKeyEvent *ev)
281{
282 const int key = ev->key();
283 if (key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_PageDown || key == Qt::Key_PageUp) {
284 // QListView/QAIV PageUp/PageDown moves the current item, first inside visible bounds
285 // before it triggers scrolling around. Let's just let the scrollarea handle it,
286 // since we don't show the current item.
288 ev->accept();
289 } else if (ev->modifiers() & Qt::ControlModifier) {
290 if (key == Qt::Key_Home) {
291 scrollToTop();
292 ev->accept();
293 } else if (key == Qt::Key_End) {
295 ev->accept();
296 }
297 }
298}
299
300void TextAutogenerateListView::slotGoToDiscussion(const QByteArray &uuid)
301{
302 const QModelIndex idx = TextAutogenerateManager::self()->textAutoGenerateChatModel()->indexForUuid(uuid);
303 if (idx.isValid()) {
304 scrollTo(idx);
305 }
306}
307
308void TextAutogenerateListView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint hint)
309{
310 QListView::scrollTo(index, hint);
311 addSelectedMessageBackgroundAnimation(index);
312}
313
314void TextAutogenerateListView::editingFinished(const QByteArray &uuid)
315{
316 const QModelIndex idx = TextAutogenerateManager::self()->textAutoGenerateChatModel()->indexForUuid(uuid);
317 if (idx.isValid()) {
318 auto lastModel = const_cast<QAbstractItemModel *>(idx.model());
319 lastModel->setData(idx, false, TextAutoGenerateChatModel::EditingRole);
320 }
321}
322
323void TextAutogenerateListView::addWaitingAnswerAnimation(const QModelIndex &index)
324{
325 auto animation = new TextAutogenerateMessageWaitingAnswerAnimation(this);
326 animation->setModelIndex(index);
327 connect(animation, &TextAutogenerateMessageWaitingAnswerAnimation::valueChanged, this, [this, animation]() {
328 mDelegate->needUpdateWaitingAnswerAnimation(animation->modelIndex(), animation->scaleOpacities());
329 update(animation->modelIndex());
330 });
331 connect(this, &TextAutogenerateListView::waitingAnswerDone, this, [this, animation](const QModelIndex &index) {
332 animation->stopAndDelete();
333 mDelegate->removeNeedUpdateWaitingAnswerAnimation(index);
334 update(index);
335 });
336 animation->start();
337}
338
339void TextAutogenerateListView::addSelectedMessageBackgroundAnimation(const QModelIndex &index)
340{
341 auto animation = new TextAutogenerateSelectedMessageBackgroundAnimation(this);
342 animation->setModelIndex(index);
343 connect(animation, &TextAutogenerateSelectedMessageBackgroundAnimation::backgroundColorChanged, this, [this, animation]() {
344 mDelegate->needUpdateIndexBackground(animation->modelIndex(), animation->backgroundColor());
345 update(animation->modelIndex());
346 });
347 connect(animation, &TextAutogenerateSelectedMessageBackgroundAnimation::animationFinished, this, [this, animation]() {
348 mDelegate->removeNeedUpdateIndexBackground(animation->modelIndex());
349 update(animation->modelIndex());
350 });
351 animation->start();
352}
353#include "moc_textautogeneratelistview.cpp"
QString i18nc(const char *context, const char *text, const TYPE &arg...)
void update(Part *part, const QByteArray &data, qint64 dataSize)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void modelAboutToBeReset()
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
virtual bool event(QEvent *event) override
QAbstractItemModel * model() const const
virtual void keyPressEvent(QKeyEvent *e) override
QScrollBar * verticalScrollBar() const const
QWidget * viewport() const const
void setPageStep(int)
void rangeChanged(int min, int max)
void triggered(bool checked)
bool isEmpty() const const
void setText(const QString &text, Mode mode)
void accept()
QClipboard * clipboard()
QIcon fromTheme(const QString &name)
int key() const const
Qt::KeyboardModifiers modifiers() const const
bool contains(const AT &value) const const
virtual QModelIndex indexAt(const QPoint &p) const const override
virtual void initViewItemOption(QStyleOptionViewItem *option) const const override
virtual void resizeEvent(QResizeEvent *e) override
virtual void scrollTo(const QModelIndex &index, ScrollHint hint) override
virtual QRect visualRect(const QModelIndex &index) const const override
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isValid() const const
ControlModifier
ScrollBarAlwaysOff
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool toBool() const const
QByteArray toByteArray() const const
QString toString() const const
virtual void leaveEvent(QEvent *event)
void update()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:06:03 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.