Changeset View
Changeset View
Standalone View
Standalone View
src/widgets/room/delegate/messagedelegatehelpertext.cpp
Show First 20 Lines • Show All 120 Lines • ▼ Show 20 Line(s) | |||||
121 | static bool useItalicsForMessage(const QModelIndex &index) | 121 | static bool useItalicsForMessage(const QModelIndex &index) | ||
122 | { | 122 | { | ||
123 | const Message::MessageType messageType = index.data(MessageModel::MessageType).value<Message::MessageType>(); | 123 | const Message::MessageType messageType = index.data(MessageModel::MessageType).value<Message::MessageType>(); | ||
124 | const bool isSystemMessage = messageType == Message::System | 124 | const bool isSystemMessage = messageType == Message::System | ||
125 | && index.data(MessageModel::SystemMessageType).toString() != QStringLiteral("jitsi_call_started"); | 125 | && index.data(MessageModel::SystemMessageType).toString() != QStringLiteral("jitsi_call_started"); | ||
126 | return isSystemMessage || messageType == Message::Video || messageType == Message::Audio; | 126 | return isSystemMessage || messageType == Message::Video || messageType == Message::Audio; | ||
127 | } | 127 | } | ||
128 | 128 | | |||
129 | // QTextDocument lacks a move constructor | | |||
130 | static void fillTextDocument(const QModelIndex &index, QTextDocument &doc, const QString &text, int width) | | |||
131 | { | | |||
132 | doc.setHtml(text); | | |||
133 | doc.setTextWidth(width); | | |||
134 | QFont font = doc.defaultFont(); | | |||
135 | font.setItalic(useItalicsForMessage(index)); | | |||
136 | doc.setDefaultFont(font); | | |||
137 | QTextFrame *frame = doc.frameAt(0); | | |||
138 | QTextFrameFormat frameFormat = frame->frameFormat(); | | |||
139 | frameFormat.setMargin(0); | | |||
140 | frame->setFrameFormat(frameFormat); | | |||
141 | } | | |||
142 | | ||||
143 | void MessageDelegateHelperText::draw(QPainter *painter, const QRect &rect, const QModelIndex &index, const QStyleOptionViewItem &option) | 129 | void MessageDelegateHelperText::draw(QPainter *painter, const QRect &rect, const QModelIndex &index, const QStyleOptionViewItem &option) | ||
144 | { | 130 | { | ||
145 | const QString text = makeMessageText(index, option.widget); | 131 | auto *doc = documentForIndex(index, rect.width(), option.widget); | ||
146 | 132 | if (!doc) { | |||
147 | if (text.isEmpty()) { | | |||
148 | return; | 133 | return; | ||
149 | } | 134 | } | ||
150 | // Possible optimisation: store the QTextDocument into the Message itself? | 135 | | ||
151 | QTextDocument doc; | | |||
152 | QTextDocument *pDoc = &doc; | | |||
153 | QVector<QAbstractTextDocumentLayout::Selection> selections; | 136 | QVector<QAbstractTextDocumentLayout::Selection> selections; | ||
154 | if (index == mCurrentIndex) { | 137 | if (index == mCurrentIndex) { | ||
155 | pDoc = &mCurrentDocument; // optimization, not stricly necessary | | |||
156 | QTextCharFormat selectionFormat; | 138 | QTextCharFormat selectionFormat; | ||
157 | selectionFormat.setBackground(option.palette.brush(QPalette::Highlight)); | 139 | selectionFormat.setBackground(option.palette.brush(QPalette::Highlight)); | ||
158 | selectionFormat.setForeground(option.palette.brush(QPalette::HighlightedText)); | 140 | selectionFormat.setForeground(option.palette.brush(QPalette::HighlightedText)); | ||
159 | selections.append({mCurrentTextCursor, selectionFormat}); | 141 | selections.append({mCurrentTextCursor, selectionFormat}); | ||
160 | } else { | | |||
161 | fillTextDocument(index, doc, text, rect.width()); | | |||
162 | } | 142 | } | ||
163 | if (useItalicsForMessage(index)) { | 143 | if (useItalicsForMessage(index)) { | ||
164 | QTextCursor cursor(pDoc); | 144 | QTextCursor cursor(doc); | ||
165 | cursor.select(QTextCursor::Document); | 145 | cursor.select(QTextCursor::Document); | ||
166 | QTextCharFormat format; | 146 | QTextCharFormat format; | ||
167 | format.setForeground(Qt::gray); //TODO use color from theme. | 147 | format.setForeground(Qt::gray); //TODO use color from theme. | ||
168 | cursor.mergeCharFormat(format); | 148 | cursor.mergeCharFormat(format); | ||
169 | } | 149 | } | ||
170 | 150 | | |||
171 | painter->save(); | 151 | painter->save(); | ||
172 | painter->translate(rect.left(), rect.top()); | 152 | painter->translate(rect.left(), rect.top()); | ||
173 | const QRect clip(0, 0, rect.width(), rect.height()); | 153 | const QRect clip(0, 0, rect.width(), rect.height()); | ||
174 | 154 | | |||
175 | // Same as pDoc->drawContents(painter, clip) but we also set selections | 155 | // Same as pDoc->drawContents(painter, clip) but we also set selections | ||
176 | QAbstractTextDocumentLayout::PaintContext ctx; | 156 | QAbstractTextDocumentLayout::PaintContext ctx; | ||
177 | ctx.selections = selections; | 157 | ctx.selections = selections; | ||
178 | if (clip.isValid()) { | 158 | if (clip.isValid()) { | ||
179 | painter->setClipRect(clip); | 159 | painter->setClipRect(clip); | ||
180 | ctx.clip = clip; | 160 | ctx.clip = clip; | ||
181 | } | 161 | } | ||
182 | pDoc->documentLayout()->draw(painter, ctx); | 162 | doc->documentLayout()->draw(painter, ctx); | ||
183 | painter->restore(); | 163 | painter->restore(); | ||
184 | } | 164 | } | ||
185 | 165 | | |||
186 | QSize MessageDelegateHelperText::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const | 166 | QSize MessageDelegateHelperText::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const | ||
187 | { | 167 | { | ||
188 | Q_UNUSED(option) | 168 | Q_UNUSED(option) | ||
189 | const QString text = makeMessageText(index, option.widget); | 169 | auto *doc = documentForIndex(index, maxWidth, option.widget); | ||
190 | if (text.isEmpty()) { | 170 | if (!doc) { | ||
191 | return QSize(); | 171 | return QSize(); | ||
192 | } | 172 | } | ||
193 | QTextDocument doc; | 173 | const QSize size(doc->idealWidth(), doc->size().height()); // do the layouting, required by lineAt(0) below | ||
194 | fillTextDocument(index, doc, text, maxWidth); | | |||
195 | const QSize size(doc.idealWidth(), doc.size().height()); // do the layouting, required by lineAt(0) below | | |||
196 | 174 | | |||
197 | const QTextLine &line = doc.firstBlock().layout()->lineAt(0); | 175 | const QTextLine &line = doc->firstBlock().layout()->lineAt(0); | ||
198 | *pBaseLine = line.y() + line.ascent(); // relative | 176 | *pBaseLine = line.y() + line.ascent(); // relative | ||
199 | 177 | | |||
200 | return size; | 178 | return size; | ||
201 | } | 179 | } | ||
202 | 180 | | |||
203 | bool MessageDelegateHelperText::handleMouseEvent(QMouseEvent *mouseEvent, const QRect &messageRect, const QStyleOptionViewItem &option, const QModelIndex &index) | 181 | bool MessageDelegateHelperText::handleMouseEvent(QMouseEvent *mouseEvent, const QRect &messageRect, const QStyleOptionViewItem &option, const QModelIndex &index) | ||
204 | { | 182 | { | ||
205 | const QPoint pos = mouseEvent->pos() - messageRect.topLeft(); | 183 | const QPoint pos = mouseEvent->pos() - messageRect.topLeft(); | ||
206 | const QEvent::Type eventType = mouseEvent->type(); | 184 | const QEvent::Type eventType = mouseEvent->type(); | ||
207 | // Text selection | 185 | // Text selection | ||
208 | switch (eventType) { | 186 | switch (eventType) { | ||
209 | case QEvent::MouseButtonPress: | 187 | case QEvent::MouseButtonPress: | ||
210 | { | 188 | { | ||
211 | if (mCurrentIndex.isValid()) { | 189 | if (mCurrentIndex.isValid()) { | ||
212 | // The old index no longer has selection, repaint it | 190 | // The old index no longer has selection, repaint it | ||
213 | updateView(option.widget, mCurrentIndex); | 191 | updateView(option.widget, mCurrentIndex); | ||
214 | } | 192 | } | ||
215 | mCurrentIndex = index; | 193 | mCurrentIndex = index; | ||
216 | const QString text = makeMessageText(index, option.widget); | 194 | mCurrentDocument = documentForIndex(index, messageRect.width(), option.widget); | ||
217 | mCurrentDocument.clear(); | 195 | if (mCurrentDocument) { | ||
218 | fillTextDocument(index, mCurrentDocument, text, messageRect.width()); | 196 | const int charPos = mCurrentDocument->documentLayout()->hitTest(pos, Qt::FuzzyHit); | ||
219 | const int charPos = mCurrentDocument.documentLayout()->hitTest(pos, Qt::FuzzyHit); | | |||
220 | // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection | 197 | // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection | ||
221 | if (charPos != -1) { | 198 | if (charPos != -1) { | ||
222 | mCurrentTextCursor = QTextCursor(&mCurrentDocument); | 199 | mCurrentTextCursor = QTextCursor(mCurrentDocument); | ||
223 | mCurrentTextCursor.setPosition(charPos); | 200 | mCurrentTextCursor.setPosition(charPos); | ||
224 | return true; | 201 | return true; | ||
225 | } | 202 | } | ||
203 | } else { | ||||
204 | mCurrentIndex = QModelIndex(); | ||||
205 | } | ||||
226 | break; | 206 | break; | ||
227 | } | 207 | } | ||
228 | case QEvent::MouseMove: | 208 | case QEvent::MouseMove: | ||
229 | if (index == mCurrentIndex) { | 209 | if (index == mCurrentIndex && mCurrentDocument) { | ||
230 | const int charPos = mCurrentDocument.documentLayout()->hitTest(pos, Qt::FuzzyHit); | 210 | const int charPos = mCurrentDocument->documentLayout()->hitTest(pos, Qt::FuzzyHit); | ||
231 | if (charPos != -1) { | 211 | if (charPos != -1) { | ||
232 | // QWidgetTextControl also has code to support dragging, isPreediting()/commitPreedit(), selectBlockOnTripleClick | 212 | // QWidgetTextControl also has code to support dragging, isPreediting()/commitPreedit(), selectBlockOnTripleClick | ||
233 | mCurrentTextCursor.setPosition(charPos, QTextCursor::KeepAnchor); | 213 | mCurrentTextCursor.setPosition(charPos, QTextCursor::KeepAnchor); | ||
234 | return true; | 214 | return true; | ||
235 | } | 215 | } | ||
236 | } | 216 | } | ||
237 | break; | 217 | break; | ||
238 | case QEvent::MouseButtonRelease: | 218 | case QEvent::MouseButtonRelease: | ||
Show All 12 Lines | 224 | if (index == mCurrentIndex) { | |||
251 | } | 231 | } | ||
252 | } | 232 | } | ||
253 | break; | 233 | break; | ||
254 | default: | 234 | default: | ||
255 | break; | 235 | break; | ||
256 | } | 236 | } | ||
257 | // Clicks on links | 237 | // Clicks on links | ||
258 | if (eventType == QEvent::MouseButtonRelease) { | 238 | if (eventType == QEvent::MouseButtonRelease) { | ||
259 | // ## we should really cache that QTextDocument... | 239 | const auto *doc = documentForIndex(index, messageRect.width(), option.widget); | ||
260 | const QString text = makeMessageText(index, option.widget); | 240 | if (!doc) { | ||
261 | QTextDocument doc; | 241 | return false; | ||
262 | fillTextDocument(index, doc, text, messageRect.width()); | 242 | } | ||
263 | 243 | const QString link = doc->documentLayout()->anchorAt(pos); | |||
264 | const QString link = doc.documentLayout()->anchorAt(pos); | | |||
265 | if (!link.isEmpty()) { | 244 | if (!link.isEmpty()) { | ||
266 | auto *rcAccount = Ruqola::self()->rocketChatAccount(); | 245 | auto *rcAccount = Ruqola::self()->rocketChatAccount(); | ||
267 | Q_EMIT rcAccount->openLinkRequested(link); | 246 | Q_EMIT rcAccount->openLinkRequested(link); | ||
268 | return true; | 247 | return true; | ||
269 | } | 248 | } | ||
270 | } | 249 | } | ||
271 | return false; | 250 | return false; | ||
272 | } | 251 | } | ||
273 | 252 | | |||
274 | bool MessageDelegateHelperText::handleHelpEvent(QHelpEvent *helpEvent, QWidget *view, const QRect &messageRect, const QModelIndex &index) | 253 | bool MessageDelegateHelperText::handleHelpEvent(QHelpEvent *helpEvent, QWidget *view, const QRect &messageRect, const QModelIndex &index) | ||
275 | { | 254 | { | ||
276 | if (helpEvent->type() != QEvent::ToolTip) { | 255 | if (helpEvent->type() != QEvent::ToolTip) { | ||
277 | return false; | 256 | return false; | ||
278 | } | 257 | } | ||
279 | 258 | | |||
280 | // ## we should really cache that QTextDocument... | 259 | const auto *doc = documentForIndex(index, messageRect.width(), view); | ||
281 | const auto text = makeMessageText(index, view); | 260 | if (!doc) { | ||
282 | QTextDocument doc; | 261 | return false; | ||
283 | fillTextDocument(index, doc, text, messageRect.width()); | 262 | } | ||
284 | 263 | | |||
285 | const QPoint pos = helpEvent->pos() - messageRect.topLeft(); | 264 | const QPoint pos = helpEvent->pos() - messageRect.topLeft(); | ||
286 | const auto format = doc.documentLayout()->formatAt(pos); | 265 | const auto format = doc->documentLayout()->formatAt(pos); | ||
287 | const auto tooltip = format.property(QTextFormat::TextToolTip).toString(); | 266 | const auto tooltip = format.property(QTextFormat::TextToolTip).toString(); | ||
288 | const auto href = format.property(QTextFormat::AnchorHref).toString(); | 267 | const auto href = format.property(QTextFormat::AnchorHref).toString(); | ||
289 | if (tooltip.isEmpty() && (href.isEmpty() || href.startsWith(QLatin1String("ruqola:/")))) { | 268 | if (tooltip.isEmpty() && (href.isEmpty() || href.startsWith(QLatin1String("ruqola:/")))) { | ||
290 | return false; | 269 | return false; | ||
291 | } | 270 | } | ||
292 | 271 | | |||
293 | QString formattedTooltip; | 272 | QString formattedTooltip; | ||
294 | QTextStream stream(&formattedTooltip); | 273 | QTextStream stream(&formattedTooltip); | ||
Show All 11 Lines | |||||
306 | QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view); | 285 | QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view); | ||
307 | return true; | 286 | return true; | ||
308 | } | 287 | } | ||
309 | 288 | | |||
310 | void MessageDelegateHelperText::setShowThreadContext(bool b) | 289 | void MessageDelegateHelperText::setShowThreadContext(bool b) | ||
311 | { | 290 | { | ||
312 | mShowThreadContext = b; | 291 | mShowThreadContext = b; | ||
313 | } | 292 | } | ||
293 | | ||||
294 | static std::unique_ptr<QTextDocument> createTextDocument(const QModelIndex &index, const QString &text, int width) | ||||
295 | { | ||||
296 | std::unique_ptr<QTextDocument> doc(new QTextDocument); | ||||
297 | doc->setHtml(text); | ||||
298 | doc->setTextWidth(width); | ||||
299 | QFont font = doc->defaultFont(); | ||||
300 | font.setItalic(useItalicsForMessage(index)); | ||||
301 | doc->setDefaultFont(font); | ||||
302 | QTextFrame *frame = doc->frameAt(0); | ||||
303 | QTextFrameFormat frameFormat = frame->frameFormat(); | ||||
304 | frameFormat.setMargin(0); | ||||
305 | frame->setFrameFormat(frameFormat); | ||||
306 | return doc; | ||||
307 | } | ||||
308 | | ||||
309 | QTextDocument * MessageDelegateHelperText::documentForIndex(const QModelIndex& index, int width, const QWidget *widget) const | ||||
310 | { | ||||
311 | const Message *message = index.data(MessageModel::MessagePointer).value<Message *>(); | ||||
312 | Q_ASSERT(message); | ||||
313 | const auto messageId = message->messageId(); | ||||
314 | Q_ASSERT(!messageId.isEmpty()); | ||||
315 | | ||||
316 | auto it = mDocumentCache.find(messageId); | ||||
317 | if (it != mDocumentCache.end()) { | ||||
318 | auto ret = it->value.get(); | ||||
319 | if (ret->textWidth() != width) { | ||||
320 | ret->setTextWidth(width); | ||||
321 | } | ||||
322 | return ret; | ||||
323 | } | ||||
324 | | ||||
325 | const QString text = makeMessageText(index, widget); | ||||
326 | if (text.isEmpty()) { | ||||
327 | return nullptr; | ||||
328 | } | ||||
329 | | ||||
330 | auto doc = createTextDocument(index, text, width); | ||||
dfaure: Nice solution to use std::unique_ptr.
This means you could do the `new QTextDocument` inside… | |||||
331 | auto ret = doc.get(); | ||||
332 | mDocumentCache.insert(messageId, std::move(doc)); | ||||
333 | return ret; | ||||
334 | } |
Nice solution to use std::unique_ptr.
This means you could do the new QTextDocument inside fillTextDocument -- which can then be renamed createTextDocument (I should have done all that),