chatmessagedelegate.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. #include "chatmessagedelegate.h"
  2. #include "chatmessage.h"
  3. #include "thememanager.h"
  4. #include <QAbstractTextDocumentLayout>
  5. #include <QFontMetrics>
  6. #include <QPainter>
  7. #include <QPainterPath>
  8. #include <QTextDocument>
  9. #include <QTextLayout>
  10. ChatMessageDelegate::ChatMessageDelegate(QObject *parent)
  11. : QStyledItemDelegate(parent)
  12. , m_viewportWidth(600)
  13. {
  14. test.setWindowTitle("test ChatMessageDelegate");
  15. //test.show();
  16. }
  17. int ChatMessageDelegate::hitTestText(const QPoint &pos, const QString &text) const
  18. {
  19. QFont fn;
  20. QFontMetrics fm(fn);
  21. if (text.isEmpty() || pos.x() < 0 || pos.y() < 0)
  22. return -1;
  23. int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
  24. - 2 * ChatConstants::BUBBLE_SPACING;
  25. // 使用QTextLayout计算文本布局
  26. QTextLayout textLayout(text);
  27. textLayout.setFont(QFont());
  28. QTextOption option;
  29. option.setWrapMode(QTextOption::WordWrap);
  30. option.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
  31. textLayout.setTextOption(option);
  32. // 计算文本布局
  33. textLayout.beginLayout();
  34. int y = 0;
  35. int lineIndex = 0;
  36. QVector<QTextLine> lines;
  37. while (true) {
  38. QTextLine line = textLayout.createLine();
  39. if (!line.isValid())
  40. break;
  41. line.setLineWidth(maxWidth);
  42. int lineHeight = line.height();
  43. line.setPosition(QPointF(0, y));
  44. lines.append(line);
  45. y += lineHeight;
  46. lineIndex++;
  47. }
  48. textLayout.endLayout();
  49. // 查找点击位置对应的行和字符
  50. for (int i = 0; i < lines.size(); ++i) {
  51. QTextLine line = lines[i];
  52. QRectF lineRect(0, line.y(), line.width(), line.height());
  53. if (pos.y() >= lineRect.top() && pos.y() <= lineRect.bottom()) {
  54. // 找到了行,现在找字符位置
  55. int xPos = qBound(0, pos.x(), int(line.width()));
  56. return line.xToCursor(xPos);
  57. }
  58. }
  59. // 如果点击在最后一行之后,返回文本末尾
  60. if (pos.y() > y && !lines.isEmpty())
  61. return text.length();
  62. return -1;
  63. }
  64. void ChatMessageDelegate::paint(QPainter *painter,
  65. const QStyleOptionViewItem &option,
  66. const QModelIndex &index) const
  67. {
  68. painter->setRenderHint(QPainter::Antialiasing);
  69. painter->setRenderHint(QPainter::TextAntialiasing);
  70. const ChatMessage message = index.data().value<ChatMessage>();
  71. // 系统消息特殊处理
  72. if (message.type == MessageType::System) {
  73. drawSystemMessage(painter, option, message);
  74. return;
  75. }
  76. // 计算各个元素的位置
  77. QSize textSize = calculateTextSize(option.fontMetrics, message.text);
  78. // 计算头像位置
  79. QRectF avatarRect;
  80. if (message.type == MessageType::Left) {
  81. avatarRect = QRectF(option.rect.left() + ChatConstants::BUBBLE_SPACING,
  82. option.rect.top(),
  83. ChatConstants::AVATAR_SIZE,
  84. ChatConstants::AVATAR_SIZE);
  85. } else {
  86. avatarRect = QRectF(option.rect.right() - ChatConstants::AVATAR_SIZE
  87. - ChatConstants::BUBBLE_SPACING,
  88. option.rect.top(),
  89. ChatConstants::AVATAR_SIZE,
  90. ChatConstants::AVATAR_SIZE);
  91. }
  92. // 计算气泡位置
  93. QRectF bubbleRect;
  94. if (message.type == MessageType::Left) {
  95. bubbleRect = QRectF(avatarRect.right() + ChatConstants::BUBBLE_SPACING,
  96. option.rect.top(),
  97. textSize.width() + 2 * ChatConstants::BUBBLE_PADDING,
  98. textSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
  99. } else {
  100. bubbleRect = QRectF(option.rect.right() - textSize.width()
  101. - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
  102. - 2 * ChatConstants::BUBBLE_SPACING,
  103. option.rect.top(),
  104. textSize.width() + 2 * ChatConstants::BUBBLE_PADDING,
  105. textSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
  106. }
  107. // 绘制气泡
  108. drawBubble(painter, bubbleRect, message.type == MessageType::Left);
  109. // 绘制头像
  110. drawAvatar(painter, avatarRect, message.avatar);
  111. // 绘制文本(带选择高亮)
  112. QRectF textRect = bubbleRect.adjusted(ChatConstants::BUBBLE_PADDING,
  113. ChatConstants::BUBBLE_PADDING,
  114. -ChatConstants::BUBBLE_PADDING,
  115. -ChatConstants::BUBBLE_PADDING);
  116. painter->setPen(ThemeManager::instance().color("colorText"));
  117. // 检查是否是当前选中的消息
  118. bool isCurrentMessage = (index == m_currentMessageIndex);
  119. drawTextWithSelection(painter, textRect, message.text, option.fontMetrics, isCurrentMessage);
  120. // // 绘制时间戳
  121. // QRectF timestampRect(bubbleRect.left(),
  122. // bubbleRect.bottom(),
  123. // bubbleRect.width(),
  124. // ChatConstants::TIMESTAMP_HEIGHT);
  125. // painter->setPen(ThemeManager::instance().color("colorTextSecondary"));
  126. // painter->setFont(QFont(option.font.family(), option.font.pointSize() - 2));
  127. // painter->drawText(timestampRect, Qt::AlignCenter, message.timestamp.toString("HH:mm"));
  128. }
  129. QSize ChatMessageDelegate::sizeHint(const QStyleOptionViewItem &option,
  130. const QModelIndex &index) const
  131. {
  132. const ChatMessage message = index.data().value<ChatMessage>();
  133. QSize textSize = calculateTextSize(option.fontMetrics, message.text);
  134. // 系统消息特殊处理
  135. if (message.type == MessageType::System) {
  136. int width = qMin(textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, viewportWidth() / 2);
  137. int height = textSize.height() + 2 * ChatConstants::BUBBLE_PADDING
  138. + ChatConstants::TIMESTAMP_HEIGHT + 2 * ChatConstants::BUBBLE_SPACING;
  139. return QSize(width, height);
  140. }
  141. int width = textSize.width() + 2 * ChatConstants::BUBBLE_PADDING + ChatConstants::AVATAR_SIZE
  142. + 2 * ChatConstants::BUBBLE_SPACING;
  143. int height = qMax(textSize.height() + 2 * ChatConstants::BUBBLE_PADDING,
  144. ChatConstants::AVATAR_SIZE)
  145. + ChatConstants::TIMESTAMP_HEIGHT;
  146. return QSize(qMin(width, viewportWidth()), height + ChatConstants::BUBBLE_SPACING);
  147. }
  148. void ChatMessageDelegate::setViewportWidth(int width)
  149. {
  150. m_viewportWidth = width - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
  151. - 2 * ChatConstants::BUBBLE_SPACING;
  152. }
  153. void ChatMessageDelegate::drawBubble(QPainter *painter, const QRectF &rect, bool isLeft) const
  154. {
  155. QPainterPath path;
  156. path.addRoundedRect(rect, ChatConstants::BUBBLE_RADIUS, ChatConstants::BUBBLE_RADIUS);
  157. // 添加小三角
  158. const int triangleSize = 6;
  159. if (isLeft) {
  160. path.moveTo(rect.left(), rect.top() + ChatConstants::AVATAR_SIZE / 2 - triangleSize);
  161. path.lineTo(rect.left() - triangleSize, rect.top() + ChatConstants::AVATAR_SIZE / 2);
  162. path.lineTo(rect.left(), rect.top() + ChatConstants::AVATAR_SIZE / 2 + triangleSize);
  163. } else {
  164. path.moveTo(rect.right(), rect.top() + ChatConstants::AVATAR_SIZE / 2 - triangleSize);
  165. path.lineTo(rect.right() + triangleSize, rect.top() + ChatConstants::AVATAR_SIZE / 2);
  166. path.lineTo(rect.right(), rect.top() + ChatConstants::AVATAR_SIZE / 2 + triangleSize);
  167. }
  168. // 设置气泡颜色
  169. if (isLeft) {
  170. painter->fillPath(path, ThemeManager::instance().color("colorFillQuaternary"));
  171. } else {
  172. painter->fillPath(path, ThemeManager::instance().color("colorPrimaryBg"));
  173. }
  174. }
  175. void ChatMessageDelegate::drawAvatar(QPainter *painter,
  176. const QRectF &rect,
  177. const QString &avatarPath) const
  178. {
  179. // 检查缓存中是否已有头像
  180. if (!m_avatarCache.contains(avatarPath)) {
  181. QPixmap avatar(avatarPath);
  182. if (!avatar.isNull()) {
  183. avatar = avatar.scaled(ChatConstants::AVATAR_SIZE,
  184. ChatConstants::AVATAR_SIZE,
  185. Qt::KeepAspectRatio,
  186. Qt::SmoothTransformation);
  187. const_cast<ChatMessageDelegate *>(this)->m_avatarCache.insert(avatarPath, avatar);
  188. }
  189. }
  190. // 创建圆形裁剪区域
  191. QPainterPath path;
  192. path.addEllipse(rect);
  193. painter->setClipPath(path);
  194. if (m_avatarCache.contains(avatarPath)) {
  195. // 绘制缓存的头像
  196. painter->drawPixmap(rect.toRect(), m_avatarCache[avatarPath]);
  197. } else {
  198. // 绘制默认头像
  199. painter->setPen(Qt::NoPen);
  200. painter->setBrush(ThemeManager::instance().color("colorFill"));
  201. painter->drawEllipse(rect);
  202. }
  203. painter->setClipping(false);
  204. }
  205. void ChatMessageDelegate::drawSystemMessage(QPainter *painter,
  206. const QStyleOptionViewItem &option,
  207. const ChatMessage &message) const
  208. {
  209. // 为系统消息单独计算文本大小
  210. int maxWidth = viewportWidth() / (2.0 / 3.0);
  211. // 使用QTextDocument计算系统消息的实际大小
  212. QFont font = painter->font();
  213. int size = font.pointSize();
  214. font.setPointSize(std::max(size - 2, 8));
  215. QTextDocument doc;
  216. doc.setDefaultFont(font);
  217. doc.setDocumentMargin(0);
  218. doc.setTextWidth(maxWidth - 2 * ChatConstants::BUBBLE_PADDING);
  219. doc.setHtml(message.text.toHtmlEscaped().replace("\n", "<br>"));
  220. QSize textSize(doc.idealWidth(), doc.size().height());
  221. // 系统消息居中显示
  222. int bubbleWidth = qMin(textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, maxWidth);
  223. // 计算气泡位置(居中)
  224. QRectF bubbleRect(option.rect.left() + (option.rect.width() - bubbleWidth) / 2,
  225. option.rect.top() + ChatConstants::BUBBLE_SPACING,
  226. bubbleWidth,
  227. textSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
  228. // 绘制系统消息气泡(使用特殊样式)
  229. QPainterPath path;
  230. path.addRoundedRect(bubbleRect, ChatConstants::BUBBLE_RADIUS, ChatConstants::BUBBLE_RADIUS);
  231. // 使用半透明灰色背景
  232. painter->fillPath(path, QColor(200, 200, 200, 60));
  233. // 绘制文本
  234. QRectF textRect = bubbleRect.adjusted(ChatConstants::BUBBLE_PADDING,
  235. ChatConstants::BUBBLE_PADDING,
  236. -ChatConstants::BUBBLE_PADDING,
  237. -ChatConstants::BUBBLE_PADDING);
  238. // 使用特殊颜色
  239. painter->setPen(QColor(255, 0, 0));
  240. QTextCharFormat defaultFormat;
  241. defaultFormat.setForeground(ThemeManager::instance().color("colorText"));
  242. QTextCursor cursor(&doc);
  243. cursor.select(QTextCursor::Document);
  244. cursor.mergeCharFormat(defaultFormat);
  245. painter->save();
  246. painter->translate(textRect.topLeft());
  247. doc.drawContents(painter);
  248. painter->restore();
  249. // // // 绘制时间戳
  250. // QRectF timestampRect(bubbleRect.left(),
  251. // bubbleRect.bottom(),
  252. // bubbleRect.width(),
  253. // ChatConstants::TIMESTAMP_HEIGHT);
  254. // painter->setPen(ThemeManager::instance().color("colorTextSecondary"));
  255. // painter->setFont(QFont(option.font.family(), option.font.pointSize() - 2));
  256. // painter->drawText(timestampRect, Qt::AlignCenter, message.timestamp.toString("HH:mm"));
  257. }
  258. // 添加绘制带选择的文本方法
  259. void ChatMessageDelegate::drawTextWithSelection(QPainter *painter,
  260. const QRectF &rect,
  261. const QString &text,
  262. const QFontMetrics &fm,
  263. bool isCurrentMessage) const
  264. {
  265. // 如果不是当前消息或没有选择,直接绘制文本
  266. // 使用QTextDocument绘制多行文本
  267. QTextDocument doc;
  268. doc.setDefaultFont(painter->font());
  269. doc.setDocumentMargin(0);
  270. doc.setTextWidth(rect.width());
  271. doc.setHtml(text.toHtmlEscaped().replace("\n", "<br>"));
  272. // 设置默认文本格式(应用主题颜色)
  273. QTextCharFormat defaultFormat;
  274. defaultFormat.setForeground(ThemeManager::instance().color("colorText"));
  275. if (!isCurrentMessage || m_selectionStart < 0 || m_selectionEnd < 0
  276. || m_selectionStart == m_selectionEnd) {
  277. // 将普通文本转换为HTML,保留换行符
  278. QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
  279. doc.setHtml(htmlText);
  280. // 应用默认格式到整个文档
  281. QTextCursor cursor(&doc);
  282. cursor.select(QTextCursor::Document);
  283. cursor.mergeCharFormat(defaultFormat);
  284. painter->save();
  285. painter->translate(rect.topLeft());
  286. painter->setPen(ThemeManager::instance().color("colorText"));
  287. doc.drawContents(painter);
  288. painter->restore();
  289. return;
  290. }
  291. // 确保选择范围有效
  292. int start = qMin(m_selectionStart, m_selectionEnd);
  293. int end = qMax(m_selectionStart, m_selectionEnd);
  294. if (start >= text.length() || end <= 0) {
  295. QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
  296. doc.setHtml(htmlText);
  297. // 应用默认格式到整个文档
  298. QTextCursor cursor(&doc);
  299. cursor.select(QTextCursor::Document);
  300. cursor.mergeCharFormat(defaultFormat);
  301. painter->save();
  302. painter->translate(rect.topLeft());
  303. painter->setPen(ThemeManager::instance().color("colorText"));
  304. doc.drawContents(painter);
  305. painter->restore();
  306. return;
  307. }
  308. // 限制选择范围在文本长度内
  309. start = qMax(0, start);
  310. end = qMin(text.length(), end);
  311. // 将普通文本转换为HTML,保留换行符
  312. QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
  313. doc.setHtml(htmlText);
  314. // 首先应用默认格式到整个文档
  315. QTextCursor cursor(&doc);
  316. cursor.select(QTextCursor::Document);
  317. cursor.mergeCharFormat(defaultFormat);
  318. // 设置选择格式
  319. cursor.setPosition(start);
  320. cursor.setPosition(end, QTextCursor::KeepAnchor);
  321. QTextCharFormat selectionFormat;
  322. selectionFormat.setBackground(QColor(0, 120, 215, 128)); // 半透明蓝色背景
  323. selectionFormat.setForeground(Qt::white); // 白色文本
  324. cursor.mergeCharFormat(selectionFormat);
  325. // 绘制文本
  326. painter->save();
  327. painter->translate(rect.topLeft());
  328. doc.drawContents(painter);
  329. painter->restore();
  330. }
  331. QSize ChatMessageDelegate::calculateTextSize(const QFontMetrics &fm, const QString &text) const
  332. {
  333. int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
  334. - 2 * ChatConstants::BUBBLE_SPACING;
  335. QRect textRect = fm.boundingRect(0,
  336. 0,
  337. maxWidth,
  338. INT_MAX,
  339. Qt::TextWordWrap | Qt::AlignLeft | Qt::AlignVCenter,
  340. text);
  341. return textRect.size();
  342. }
  343. int ChatMessageDelegate::getPositionFromPoint(const QPoint &pos,
  344. const QString &text,
  345. const QFontMetrics &fm)
  346. {
  347. if (text.isEmpty() || pos.x() < 0 || pos.y() < 0)
  348. return -1;
  349. int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING;
  350. // 使用QTextDocument处理多行文本
  351. QTextDocument doc;
  352. doc.setDefaultFont(QFont());
  353. doc.setDocumentMargin(0);
  354. doc.setTextWidth(maxWidth - 52); // AVATAR_SIZE + BUBBLE_PADDING
  355. doc.setHtml(text.toHtmlEscaped().replace("\n", "<br>"));
  356. test.setDocument(&doc);
  357. // 获取点击位置对应的文本位置
  358. QAbstractTextDocumentLayout *layout = doc.documentLayout();
  359. int position = layout->hitTest(pos, Qt::FuzzyHit);
  360. return position;
  361. }