chatwindow.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. #include "chatwindow.h"
  2. #include <QAction>
  3. #include <QApplication>
  4. #include <QButtonGroup>
  5. #include <QClipboard>
  6. #include <QDir>
  7. #include <QFileInfo>
  8. #include <QHeaderView>
  9. #include <QImageReader>
  10. #include <QMenu>
  11. #include <QMessageBox>
  12. #include <QMimeData>
  13. #include <QScrollBar>
  14. #include <QSplitter>
  15. #include <QStandardPaths>
  16. #include <QTableWidget>
  17. #include <QTextDocumentFragment>
  18. #include <QTextImageFormat>
  19. #include <QUuid>
  20. #include "appevent.h"
  21. #include "qpushbutton.h"
  22. #include "widgets/chatView/chat1/chatview.h"
  23. // MultiFormatInputEdit 实现
  24. MultiFormatInputEdit::MultiFormatInputEdit(QWidget *parent)
  25. : QTextEdit(parent)
  26. {
  27. setAcceptDrops(true);
  28. setPlaceholderText("输入消息... (支持粘贴图片)");
  29. setMaximumHeight(120);
  30. setMinimumHeight(60);
  31. // 连接文本变化信号
  32. connect(this, &QTextEdit::textChanged, this, &MultiFormatInputEdit::onTextChanged);
  33. }
  34. QString MultiFormatInputEdit::getPlainText() const
  35. {
  36. // 获取纯文本,但保留图片占位符信息
  37. QString text = toPlainText().trimmed();
  38. return text;
  39. }
  40. QStringList MultiFormatInputEdit::getImagePaths() const
  41. {
  42. return m_imagePaths;
  43. }
  44. void MultiFormatInputEdit::insertImage(const QString &imagePath)
  45. {
  46. if (imagePath.isEmpty() || m_imagePaths.contains(imagePath))
  47. return;
  48. m_imagePaths.append(imagePath);
  49. insertImageIntoDocument(imagePath);
  50. emit imageInserted(imagePath);
  51. emit contentChanged();
  52. }
  53. void MultiFormatInputEdit::clearContent()
  54. {
  55. clear();
  56. m_imagePaths.clear();
  57. m_imageResourceMap.clear();
  58. document()->clear();
  59. emit contentChanged();
  60. }
  61. bool MultiFormatInputEdit::validateContent() const
  62. {
  63. QString text = getPlainText();
  64. // 检查是否有内容
  65. if (text.isEmpty() && m_imagePaths.isEmpty()) {
  66. return false;
  67. }
  68. // 检查文本长度
  69. if (text.length() > 5000) {
  70. return false;
  71. }
  72. // 检查图片文件是否存在
  73. for (const QString &imagePath : m_imagePaths) {
  74. if (!QFileInfo::exists(imagePath)) {
  75. return false;
  76. }
  77. }
  78. return true;
  79. }
  80. void MultiFormatInputEdit::removeImage(const QString &imagePath)
  81. {
  82. if (!m_imagePaths.contains(imagePath))
  83. return;
  84. m_imagePaths.removeAll(imagePath);
  85. // 从文档中移除图片资源
  86. if (m_imageResourceMap.contains(imagePath)) {
  87. QString resourceName = m_imageResourceMap[imagePath];
  88. document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), QVariant());
  89. m_imageResourceMap.remove(imagePath);
  90. }
  91. emit imageRemoved(imagePath);
  92. emit contentChanged();
  93. }
  94. void MultiFormatInputEdit::dragEnterEvent(QDragEnterEvent *event)
  95. {
  96. if (event->mimeData()->hasImage() || event->mimeData()->hasUrls()) {
  97. event->acceptProposedAction();
  98. } else {
  99. QTextEdit::dragEnterEvent(event);
  100. }
  101. }
  102. void MultiFormatInputEdit::dropEvent(QDropEvent *event)
  103. {
  104. const QMimeData *mimeData = event->mimeData();
  105. if (mimeData->hasUrls()) {
  106. const QList<QUrl> urls = mimeData->urls();
  107. for (const QUrl &url : urls) {
  108. if (url.isLocalFile()) {
  109. QString filePath = url.toLocalFile();
  110. if (isImageFormat(filePath)) {
  111. insertImage(filePath);
  112. }
  113. }
  114. }
  115. event->acceptProposedAction();
  116. } else if (mimeData->hasImage()) {
  117. QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());
  118. if (!pixmap.isNull()) {
  119. QString tempPath = saveImageToTemp(pixmap);
  120. if (!tempPath.isEmpty()) {
  121. insertImage(tempPath);
  122. }
  123. }
  124. event->acceptProposedAction();
  125. } else {
  126. QTextEdit::dropEvent(event);
  127. }
  128. }
  129. void MultiFormatInputEdit::keyPressEvent(QKeyEvent *event)
  130. {
  131. if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
  132. if (event->modifiers() & Qt::ControlModifier) {
  133. // Ctrl+Enter 发送消息
  134. emit sendRequested();
  135. return;
  136. } else {
  137. // 普通回车换行
  138. QTextEdit::keyPressEvent(event);
  139. }
  140. } else {
  141. QTextEdit::keyPressEvent(event);
  142. }
  143. }
  144. void MultiFormatInputEdit::insertFromMimeData(const QMimeData *source)
  145. {
  146. if (source->hasImage()) {
  147. QPixmap pixmap = qvariant_cast<QPixmap>(source->imageData());
  148. if (!pixmap.isNull()) {
  149. QString tempPath = saveImageToTemp(pixmap);
  150. if (!tempPath.isEmpty()) {
  151. insertImage(tempPath);
  152. }
  153. }
  154. } else {
  155. QTextEdit::insertFromMimeData(source);
  156. }
  157. }
  158. void MultiFormatInputEdit::contextMenuEvent(QContextMenuEvent *event)
  159. {
  160. QMenu *menu = createStandardContextMenu();
  161. // 检查是否点击在图片上
  162. QTextCursor cursor = cursorForPosition(event->pos());
  163. QTextCharFormat format = cursor.charFormat();
  164. if (format.isImageFormat()) {
  165. QTextImageFormat imageFormat = format.toImageFormat();
  166. QString imageName = imageFormat.name();
  167. // 查找对应的图片路径
  168. QString imagePath;
  169. for (auto it = m_imageResourceMap.begin(); it != m_imageResourceMap.end(); ++it) {
  170. if (it.value() == imageName) {
  171. imagePath = it.key();
  172. break;
  173. }
  174. }
  175. if (!imagePath.isEmpty()) {
  176. menu->addSeparator();
  177. QAction *removeAction = menu->addAction("删除图片");
  178. connect(removeAction, &QAction::triggered, [this, imagePath]() {
  179. removeImage(imagePath);
  180. });
  181. }
  182. }
  183. menu->exec(event->globalPos());
  184. delete menu;
  185. }
  186. void MultiFormatInputEdit::onTextChanged()
  187. {
  188. emit contentChanged();
  189. }
  190. void MultiFormatInputEdit::processImageData(const QByteArray &imageData, const QString &format)
  191. {
  192. QPixmap pixmap;
  193. if (pixmap.loadFromData(imageData, format.toUtf8().constData())) {
  194. QString tempPath = saveImageToTemp(pixmap, format);
  195. if (!tempPath.isEmpty()) {
  196. insertImage(tempPath);
  197. }
  198. }
  199. }
  200. bool MultiFormatInputEdit::isImageFormat(const QString &fileName) const
  201. {
  202. QStringList imageFormats = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"};
  203. QString suffix = QFileInfo(fileName).suffix().toLower();
  204. return imageFormats.contains(suffix);
  205. }
  206. QString MultiFormatInputEdit::saveImageToTemp(const QPixmap &pixmap, const QString &format)
  207. {
  208. QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
  209. QDir().mkpath(tempDir);
  210. QString fileName
  211. = QString("chat_image_%1.%2").arg(QDateTime::currentMSecsSinceEpoch()).arg(format.toLower());
  212. QString filePath = QDir(tempDir).absoluteFilePath(fileName);
  213. if (pixmap.save(filePath, format.toUtf8().constData())) {
  214. return filePath;
  215. }
  216. return QString();
  217. }
  218. void MultiFormatInputEdit::insertImageIntoDocument(const QString &imagePath)
  219. {
  220. QPixmap pixmap(imagePath);
  221. if (pixmap.isNull())
  222. return;
  223. // 缩放图片以适应输入框
  224. int maxWidth = width() - 20; // 留出边距
  225. int maxHeight = 80; // 限制高度
  226. if (pixmap.width() > maxWidth || pixmap.height() > maxHeight) {
  227. pixmap = pixmap.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  228. }
  229. // 生成唯一的资源名称
  230. QString resourceName = generateResourceName(imagePath);
  231. m_imageResourceMap[imagePath] = resourceName;
  232. // 将图片添加到文档资源
  233. document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), pixmap);
  234. // 在光标位置插入图片
  235. QTextCursor cursor = textCursor();
  236. QTextImageFormat imageFormat;
  237. imageFormat.setName(resourceName);
  238. imageFormat.setWidth(pixmap.width());
  239. imageFormat.setHeight(pixmap.height());
  240. cursor.insertImage(imageFormat);
  241. cursor.insertText(" "); // 在图片后添加空格,方便编辑
  242. setTextCursor(cursor);
  243. }
  244. QString MultiFormatInputEdit::generateResourceName(const QString &imagePath)
  245. {
  246. return QString("image_%1_%2")
  247. .arg(QUuid::createUuid().toString(QUuid::WithoutBraces))
  248. .arg(QFileInfo(imagePath).baseName());
  249. }
  250. // ChatWindow 实现
  251. ChatWindow::ChatWindow(WebSocketClient *webSocketClient, QWidget *parent)
  252. : QWidget(parent)
  253. , m_webSocketClient(webSocketClient)
  254. , m_quickButtonContainer(nullptr)
  255. , m_hideTimer(new QTimer(this))
  256. {
  257. // 设置定时器为单次触发
  258. m_hideTimer->setSingleShot(true);
  259. connect(m_hideTimer, &QTimer::timeout, this, &ChatWindow::hideQuickButtons);
  260. setupUI();
  261. connectSignals();
  262. }
  263. void ChatWindow::setupUI()
  264. {
  265. setWindowTitle("聊天窗口");
  266. resize(800, 600);
  267. QVBoxLayout *mainLayout = new QVBoxLayout(this);
  268. // 消息显示区域
  269. m_messageView = new ChatView(this);
  270. mainLayout->addWidget(m_messageView, 1);
  271. setupInputArea();
  272. }
  273. void ChatWindow::setupInputArea()
  274. {
  275. // 输入区域容器
  276. QWidget *inputContainer = new QWidget;
  277. QVBoxLayout *inputLayout = new QVBoxLayout(inputContainer);
  278. inputLayout->setContentsMargins(5, 5, 5, 5);
  279. inputLayout->setSpacing(5);
  280. // 输入框
  281. m_inputEdit = new MultiFormatInputEdit(this);
  282. // 按钮区域
  283. QHBoxLayout *buttonLayout = new QHBoxLayout;
  284. buttonLayout->addStretch();
  285. QButtonGroup *group = new QButtonGroup(this);
  286. m_quickButtonGroup = group; // 保存引用以便后续连接信号
  287. QHBoxLayout *quickButtons = new QHBoxLayout;
  288. quickButtons->addSpacing(5);
  289. for (int var = 0; var < 7; ++var) {
  290. auto button = new QPushButton(QString::number(var), this);
  291. group->addButton(button);
  292. quickButtons->addWidget(button);
  293. }
  294. // 创建快捷按钮容器并设置初始状态为隐藏
  295. m_quickButtonContainer = new QWidget(this);
  296. m_quickButtonContainer->setLayout(quickButtons);
  297. m_quickButtonContainer->setVisible(false);
  298. m_imageButton = new QPushButton("图片", this);
  299. m_fileButton = new QPushButton("文件", this);
  300. m_streamButton = new QPushButton("推流", this);
  301. m_sendButton = new QPushButton("发送", this);
  302. buttonLayout->addWidget(m_imageButton);
  303. buttonLayout->addWidget(m_fileButton);
  304. buttonLayout->addWidget(m_streamButton);
  305. buttonLayout->addWidget(m_sendButton);
  306. inputLayout->addWidget(m_quickButtonContainer);
  307. inputLayout->addWidget(m_inputEdit);
  308. inputLayout->addLayout(buttonLayout);
  309. // 添加到主布局
  310. layout()->addWidget(inputContainer);
  311. // 隐藏文件按钮(暂未实现)
  312. m_fileButton->hide();
  313. }
  314. void ChatWindow::connectSignals()
  315. {
  316. // 输入框信号
  317. connect(m_inputEdit, &MultiFormatInputEdit::sendRequested, this, &ChatWindow::onSendClicked);
  318. connect(m_inputEdit,
  319. &MultiFormatInputEdit::contentChanged,
  320. this,
  321. &ChatWindow::onInputContentChanged);
  322. connect(m_inputEdit, &MultiFormatInputEdit::imageInserted, this, &ChatWindow::onImageInserted);
  323. // 按钮信号
  324. connect(m_sendButton, &QPushButton::clicked, this, &ChatWindow::onSendClicked);
  325. connect(m_imageButton, &QPushButton::clicked, this, &ChatWindow::onImageClicked);
  326. connect(m_fileButton, &QPushButton::clicked, this, &ChatWindow::onFileClicked);
  327. connect(m_streamButton, &QPushButton::clicked, this, &ChatWindow::onStreamButtonClicked);
  328. // 快速按钮信号连接
  329. if (m_quickButtonGroup) {
  330. const QList<QAbstractButton *> buttons = m_quickButtonGroup->buttons();
  331. for (QAbstractButton *button : buttons) {
  332. connect(button, &QAbstractButton::clicked, this, &ChatWindow::onQuickButtonClicked);
  333. }
  334. }
  335. // WebSocket信号
  336. if (m_webSocketClient) {
  337. connect(m_webSocketClient,
  338. &WebSocketClient::messageReceived,
  339. [this](const ChatMessage &message) {
  340. if (message.hasImage()) {
  341. // 处理图片消息
  342. if (message.isLeft()) {
  343. m_messageView->addImageMessage(message.imagePath,
  344. message.avatar,
  345. message.senderName,
  346. true,
  347. message.text);
  348. } else {
  349. m_messageView->addImageMessage(message.imagePath,
  350. message.avatar,
  351. message.senderName,
  352. false,
  353. message.text);
  354. }
  355. } else {
  356. // 处理纯文本消息
  357. if (message.isLeft()) {
  358. m_messageView->addMessage(message.text,
  359. message.avatar,
  360. message.senderName,
  361. true);
  362. } else {
  363. m_messageView->addMessage(message.text,
  364. message.avatar,
  365. message.senderName,
  366. false);
  367. }
  368. }
  369. });
  370. connect(m_webSocketClient, &WebSocketClient::connectionStateChanged, [this](bool connected) {
  371. if (connected) {
  372. m_messageView->addSystemMessage("已连接到服务器");
  373. } else {
  374. m_messageView->addSystemMessage("与服务器断开连接");
  375. }
  376. });
  377. // 连接推流通知信号
  378. connect(m_webSocketClient, &WebSocketClient::streamNotification, this, &ChatWindow::onStreamNotification);
  379. }
  380. }
  381. void ChatWindow::initWebsocket(const QString &roomId)
  382. {
  383. m_roomId = roomId;
  384. if (m_webSocketClient) {
  385. m_webSocketClient->connectToRoom(roomId.isEmpty() ? "default_room" : roomId);
  386. }
  387. }
  388. void ChatWindow::onSendClicked()
  389. {
  390. if (validateInput()) {
  391. sendMessage();
  392. }
  393. }
  394. void ChatWindow::onImageClicked()
  395. {
  396. QString fileName
  397. = QFileDialog::getOpenFileName(this,
  398. "选择图片",
  399. "",
  400. "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp)");
  401. if (!fileName.isEmpty()) {
  402. m_inputEdit->insertImage(fileName);
  403. }
  404. }
  405. void ChatWindow::onFileClicked()
  406. {
  407. // 文件发送功能待实现
  408. QMessageBox::information(this, "提示", "文件发送功能暂未实现");
  409. }
  410. void ChatWindow::onRecallClicked()
  411. {
  412. // 撤回消息功能待实现
  413. }
  414. void ChatWindow::onInputContentChanged()
  415. {
  416. // 更新发送按钮状态
  417. m_sendButton->setEnabled(m_inputEdit->validateContent());
  418. }
  419. void ChatWindow::onImageInserted(const QString &imagePath)
  420. {
  421. Q_UNUSED(imagePath)
  422. // 图片已直接显示在输入框中,无需额外处理
  423. }
  424. void ChatWindow::onQuickButtonClicked()
  425. {
  426. QPushButton *button = qobject_cast<QPushButton *>(sender());
  427. if (button) {
  428. QString buttonText = button->text();
  429. // 发送按钮上的数字作为消息
  430. m_messageView->addMessage(buttonText, "", "我", false);
  431. if (m_webSocketClient) {
  432. m_webSocketClient->sendMessage(buttonText);
  433. }
  434. // 发送消息后隐藏快捷按钮
  435. hideQuickButtons();
  436. }
  437. }
  438. void ChatWindow::onStreamNotification(bool show)
  439. {
  440. if (show) {
  441. showQuickButtons();
  442. } else {
  443. hideQuickButtons();
  444. }
  445. }
  446. void ChatWindow::showQuickButtons()
  447. {
  448. if (m_quickButtonContainer) {
  449. m_quickButtonContainer->setVisible(true);
  450. // 启动定时器,10秒后自动隐藏
  451. m_hideTimer->start(10000);
  452. }
  453. }
  454. void ChatWindow::hideQuickButtons()
  455. {
  456. if (m_quickButtonContainer) {
  457. m_quickButtonContainer->setVisible(false);
  458. m_hideTimer->stop();
  459. }
  460. }
  461. void ChatWindow::setQuickButtonsVisible(bool visible)
  462. {
  463. if (m_quickButtonContainer) {
  464. m_quickButtonContainer->setVisible(visible);
  465. if (visible) {
  466. // 如果显示,启动定时器
  467. m_hideTimer->start(10000);
  468. } else {
  469. // 如果隐藏,停止定时器
  470. m_hideTimer->stop();
  471. }
  472. }
  473. }
  474. void ChatWindow::sendMessage()
  475. {
  476. QString text = m_inputEdit->getPlainText();
  477. QStringList imagePaths = m_inputEdit->getImagePaths();
  478. // 发送文本消息
  479. if (!text.isEmpty()) {
  480. m_messageView->addMessage(text, "", "我", false);
  481. if (m_webSocketClient) {
  482. m_webSocketClient->sendMessage(text);
  483. }
  484. }
  485. // 发送图片消息
  486. for (const QString &imagePath : imagePaths) {
  487. sendImageMessage(imagePath, text);
  488. }
  489. // 清空输入框
  490. m_inputEdit->clearContent();
  491. // 发送任意消息后隐藏快捷按钮
  492. hideQuickButtons();
  493. }
  494. void ChatWindow::sendImageMessage(const QString &imagePath, const QString &text)
  495. {
  496. if (QFileInfo::exists(imagePath)) {
  497. m_messageView->addImageMessage(imagePath, "", "我", false, text);
  498. if (m_webSocketClient) {
  499. m_webSocketClient->sendImageMessage(imagePath, text);
  500. }
  501. }
  502. }
  503. bool ChatWindow::validateInput() const
  504. {
  505. return m_inputEdit->validateContent();
  506. }
  507. void ChatWindow::closeEvent(QCloseEvent *event)
  508. {
  509. event->ignore();
  510. emit windowCloseRequested();
  511. }
  512. void ChatWindow::onStreamButtonClicked()
  513. {
  514. // 发送推流通知,显示快捷按钮
  515. if (m_webSocketClient) {
  516. m_webSocketClient->sendStreamNotification(true);
  517. }
  518. }