1 /*
2 Drawpile - a collaborative drawing program.
3
4 Copyright (C) 2007-2019 Calle Laakkonen
5
6 Drawpile is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Drawpile is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Drawpile. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "chatlineedit.h"
21 #include "chatwidgetpinnedarea.h"
22 #include "chatwidget.h"
23 #include "utils/html.h"
24 #include "utils/funstuff.h"
25 #include "notifications.h"
26
27 #include "../libshared/net/meta.h"
28 #include "canvas/userlist.h"
29
30 #include <QResizeEvent>
31 #include <QTextBrowser>
32 #include <QVBoxLayout>
33 #include <QLabel>
34 #include <QDateTime>
35 #include <QTextBlock>
36 #include <QScrollBar>
37 #include <QTabBar>
38 #include <QIcon>
39 #include <QMenu>
40 #include <QSettings>
41
42 #include <memory>
43
44 namespace widgets {
45
46 struct Chat {
47 QTextDocument *doc;
48 int lastAppendedId = 0;
49 qint64 lastMessageTs = 0;
50 int scrollPosition = 0;
51
Chatwidgets::Chat52 Chat() : doc(nullptr) { }
Chatwidgets::Chat53 explicit Chat(QObject *parent)
54 : doc(new QTextDocument(parent))
55 {
56 doc->setDefaultStyleSheet(
57 ".sep { background: #4d4d4d }"
58 ".notification { background: #232629 }"
59 ".message, .notification {"
60 "color: #eff0f1;"
61 "margin: 1px 0 1px 0"
62 "}"
63 ".shout { background: #34292c }"
64 ".shout .tab { background: #da4453 }"
65 ".action { font-style: italic }"
66 ".username { font-weight: bold }"
67 ".trusted { color: #27ae60 }"
68 ".registered { color: #16a085 }"
69 ".op { color: #f47750 }"
70 ".mod { color: #ed1515 }"
71 ".timestamp { color: #8d8d8d }"
72 "a:link { color: #1d99f3 }"
73 );
74 }
75
76 void appendSeparator(QTextCursor &cursor);
77 void appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout);
78 void appendMessageCompact(int userId, const QString &usernameSpan, const QString &message, bool shout);
79 void appendAction(const QString &usernameSpan, const QString &message);
80 void appendNotification(const QString &message);
81 };
82
83 struct ChatWidget::Private {
Privatewidgets::ChatWidget::Private84 Private(ChatWidget *parent) : chatbox(parent) { }
85
86 ChatWidget * const chatbox;
87 QTextBrowser *view = nullptr;
88 ChatLineEdit *myline = nullptr;
89 ChatWidgetPinnedArea *pinned = nullptr;
90 QTabBar *tabs = nullptr;
91
92 QList<int> announcedUsers;
93 canvas::UserListModel *userlist = nullptr;
94 QHash<int, Chat> chats;
95
96 int myId = 0;
97 int currentChat = 0;
98
99 bool preserveChat = true;
100 bool compactMode = false;
101 bool isAttached = true;
102
103 QString usernameSpan(int userId);
104
isAtEndwidgets::ChatWidget::Private105 bool isAtEnd() const {
106 return view->verticalScrollBar()->value() == view->verticalScrollBar()->maximum();
107 }
108
scrollToEndwidgets::ChatWidget::Private109 void scrollToEnd(int ifCurrentId) {
110 if(ifCurrentId == tabs->tabData(tabs->currentIndex()).toInt())
111 view->verticalScrollBar()->setValue(view->verticalScrollBar()->maximum());
112 }
113
publicChatwidgets::ChatWidget::Private114 inline Chat &publicChat()
115 {
116 Q_ASSERT(chats.contains(0));
117 return chats[0];
118 }
119
120 bool ensurePrivateChatExists(int userId, QObject *parent);
121
122 void updatePreserveModeUi();
123 };
124
ChatWidget(QWidget * parent)125 ChatWidget::ChatWidget(QWidget *parent)
126 : QWidget(parent), d(new Private(this))
127 {
128 QVBoxLayout *layout = new QVBoxLayout(this);
129
130 layout->setSpacing(0);
131 layout->setMargin(0);
132
133 d->tabs = new QTabBar(this);
134 d->tabs->addTab(QString());
135 d->tabs->setTabText(0, tr("Public"));
136 d->tabs->setAutoHide(true);
137 d->tabs->setDocumentMode(true);
138 d->tabs->setTabsClosable(true);
139 d->tabs->setMovable(true);
140 d->tabs->setTabData(0, 0); // context id 0 is used for the public chat
141
142 // The public chat cannot be closed
143 if(d->tabs->tabButton(0, QTabBar::LeftSide)) {
144 d->tabs->tabButton(0, QTabBar::LeftSide)->deleteLater();
145 d->tabs->setTabButton(0, QTabBar::LeftSide, nullptr);
146 }
147 if(d->tabs->tabButton(0, QTabBar::RightSide)) {
148 d->tabs->tabButton(0, QTabBar::RightSide)->deleteLater();
149 d->tabs->setTabButton(0, QTabBar::RightSide, nullptr);
150 }
151
152 connect(d->tabs, &QTabBar::currentChanged, this, &ChatWidget::chatTabSelected);
153 connect(d->tabs, &QTabBar::tabCloseRequested, this, &ChatWidget::chatTabClosed);
154 layout->addWidget(d->tabs, 0);
155
156 d->pinned = new ChatWidgetPinnedArea(this);
157 layout->addWidget(d->pinned, 0);
158
159 d->view = new QTextBrowser(this);
160 d->view->setOpenExternalLinks(true);
161
162 d->view->setContextMenuPolicy(Qt::CustomContextMenu);
163 connect(d->view, &QTextBrowser::customContextMenuRequested, this, &ChatWidget::showChatContextMenu);
164
165 layout->addWidget(d->view, 1);
166
167 d->myline = new ChatLineEdit(this);
168 layout->addWidget(d->myline);
169
170 setLayout(layout);
171
172 connect(d->myline, &ChatLineEdit::returnPressed, this, &ChatWidget::sendMessage);
173
174 d->chats[0] = Chat(this);
175 d->view->setDocument(d->chats[0].doc);
176
177 setPreserveMode(false);
178
179 d->compactMode = QSettings().value("history/compactchat").toBool();
180 }
181
~ChatWidget()182 ChatWidget::~ChatWidget()
183 {
184 delete d;
185 }
186
setAttached(bool isAttached)187 void ChatWidget::setAttached(bool isAttached)
188 {
189 d->isAttached = isAttached;
190 }
191
updatePreserveModeUi()192 void ChatWidget::Private::updatePreserveModeUi()
193 {
194 const bool preserve = preserveChat && currentChat == 0;
195
196 QString placeholder, color;
197 if(preserve) {
198 placeholder = tr("Chat (recorded)...");
199 color = "#da4453";
200 } else {
201 placeholder = tr("Chat...");
202 color = "#1d99f3";
203 }
204
205 // Set placeholder text and window style based on the mode
206 myline->setPlaceholderText(placeholder);
207
208 chatbox->setStyleSheet(
209 QStringLiteral(
210 "QTextEdit, QLineEdit {"
211 "background-color: #232629;"
212 "border: none;"
213 "color: #eff0f1"
214 "}"
215 "QLineEdit {"
216 "border-top: 1px solid %1;"
217 "padding: 4px"
218 "}"
219 ).arg(color)
220 );
221
222 }
setPreserveMode(bool preservechat)223 void ChatWidget::setPreserveMode(bool preservechat)
224 {
225 d->preserveChat = preservechat;
226 d->updatePreserveModeUi();
227 }
228
loggedIn(int myId)229 void ChatWidget::loggedIn(int myId)
230 {
231 d->myId = myId;
232 d->announcedUsers.clear();
233 }
234
focusInput()235 void ChatWidget::focusInput()
236 {
237 d->myline->setFocus();
238 }
239
setUserList(canvas::UserListModel * userlist)240 void ChatWidget::setUserList(canvas::UserListModel *userlist)
241 {
242 d->userlist = userlist;
243 }
244
clear()245 void ChatWidget::clear()
246 {
247 Chat &chat = d->chats[d->currentChat];
248 chat.doc->clear();
249 chat.lastAppendedId = 0;
250
251 // Re-add avatars
252 if(d->userlist) {
253 for(const auto &u : d->userlist->users()) {
254 chat.doc->addResource(
255 QTextDocument::ImageResource,
256 QUrl(QStringLiteral("avatar://%1").arg(u.id)),
257 u.avatar
258 );
259 }
260 }
261 }
262
ensurePrivateChatExists(int userId,QObject * parent)263 bool ChatWidget::Private::ensurePrivateChatExists(int userId, QObject *parent)
264 {
265 if(userId < 1 || userId > 255) {
266 qWarning("ChatWidget::openPrivateChat(%d): Invalid user ID", userId);
267 return false;
268 }
269 if(userId == myId) {
270 qWarning("ChatWidget::openPrivateChat(%d): this is me...", userId);
271 return false;
272 }
273
274 if(!chats.contains(userId)) {
275 chats[userId] = Chat(parent);
276 const int newTab = tabs->addTab(userlist->getUsername(userId));
277 tabs->setTabData(newTab, userId);
278
279 chats[userId].doc->addResource(
280 QTextDocument::ImageResource,
281 QUrl(QStringLiteral("avatar://%1").arg(userId)),
282 userlist->getUserById(userId).avatar
283 );
284 chats[userId].doc->addResource(
285 QTextDocument::ImageResource,
286 QUrl(QStringLiteral("avatar://%1").arg(myId)),
287 userlist->getUserById(myId).avatar
288 );
289 }
290
291 return true;
292 }
293
openPrivateChat(int userId)294 void ChatWidget::openPrivateChat(int userId)
295 {
296 if(!d->ensurePrivateChatExists(userId, this))
297 return;
298
299 for(int i=d->tabs->count()-1;i>=0;--i) {
300 if(d->tabs->tabData(i).toInt() == userId) {
301 d->tabs->setCurrentIndex(i);
302 break;
303 }
304 }
305 }
306
timestamp()307 static QString timestamp()
308 {
309 return QStringLiteral("<span class=ts>%1</span>").arg(
310 QDateTime::currentDateTime().toString("HH:mm")
311 );
312 }
313
usernameSpan(int userId)314 QString ChatWidget::Private::usernameSpan(int userId)
315 {
316 const canvas::User user = userlist ? userlist->getUserById(userId) : canvas::User();
317
318 QString userclass;
319 if(user.isMod)
320 userclass = QStringLiteral("mod");
321 else if(user.isOperator)
322 userclass = QStringLiteral("op");
323 else if(user.isTrusted)
324 userclass = QStringLiteral("trusted");
325 else if(user.isAuth)
326 userclass = QStringLiteral("registered");
327
328 return QStringLiteral("<span class=\"username %1\">%2</span>").arg(
329 userclass,
330 user.name.isEmpty() ? QStringLiteral("<s>User #%1</s>").arg(userId) : user.name.toHtmlEscaped()
331 );
332 }
333
appendSeparator(QTextCursor & cursor)334 void Chat::appendSeparator(QTextCursor &cursor)
335 {
336 cursor.insertHtml(QStringLiteral(
337 "<table height=1 width=\"100%\" class=sep><tr><td></td></tr></table>"
338 ));
339 }
340
appendMessageCompact(int userId,const QString & usernameSpan,const QString & message,bool shout)341 void Chat::appendMessageCompact(int userId, const QString &usernameSpan, const QString &message, bool shout)
342 {
343 Q_UNUSED(userId);
344
345 lastAppendedId = -1;
346
347 QTextCursor cursor(doc);
348
349 cursor.movePosition(QTextCursor::End);
350
351 cursor.insertHtml(QStringLiteral(
352 "<table width=\"100%\" class=\"message%1\">"
353 "<tr>"
354 "<td width=3 class=tab></td>"
355 "<td>%2: %3</td>"
356 "<td class=timestamp align=right>%4</td>"
357 "</tr>"
358 "</table>"
359 ).arg(
360 shout ? QStringLiteral(" shout") : QString(),
361 usernameSpan,
362 message,
363 timestamp()
364 )
365 );
366 }
367
appendMessage(int userId,const QString & usernameSpan,const QString & message,bool shout)368 void Chat::appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout)
369 {
370 QTextCursor cursor(doc);
371 cursor.movePosition(QTextCursor::End);
372
373 const qint64 ts = QDateTime::currentMSecsSinceEpoch();
374
375 if(shout) {
376 lastAppendedId = -2;
377
378 } else if(lastAppendedId != userId) {
379 appendSeparator(cursor);
380 lastAppendedId = userId;
381
382 } else if(ts - lastMessageTs < 60000) {
383 QTextBlock b = doc->lastBlock().previous();
384 cursor.setPosition(b.position() + b.length() - 1);
385
386 cursor.insertHtml(QStringLiteral("<br>"));
387 cursor.insertHtml(message);
388
389 return;
390 }
391
392 // We'll have to make do with a very limited subset of HTML and CSS:
393 // http://doc.qt.io/qt-5/richtext-html-subset.html
394 // Embedding a whole browser engine just to render the chat widget would
395 // be excessive.
396 cursor.insertHtml(QStringLiteral(
397 "<table width=\"100%\" class=\"message%1\">"
398 "<tr>"
399 "<td width=3 rowspan=2 class=tab></td>"
400 "<td width=40 rowspan=2><img src=\"avatar://%2\"></td>"
401 "<td>%3</td>"
402 "<td class=timestamp align=right>%4</td>"
403 "</tr>"
404 "<tr>"
405 "<td colspan=2>%5</td>"
406 "</tr>"
407 "</table>"
408 ).arg(
409 shout ? QStringLiteral(" shout") : QString(),
410 QString::number(userId),
411 usernameSpan,
412 timestamp(),
413 htmlutils::newlineToBr(message)
414 )
415 );
416 lastMessageTs = ts;
417 }
418
appendAction(const QString & usernameSpan,const QString & message)419 void Chat::appendAction(const QString &usernameSpan, const QString &message)
420 {
421 QTextCursor cursor(doc);
422 cursor.movePosition(QTextCursor::End);
423
424 if(lastAppendedId != -1) {
425 appendSeparator(cursor);
426 lastAppendedId = -1;
427 }
428 cursor.insertHtml(QStringLiteral(
429 "<table width=\"100%\" class=message>"
430 "<tr>"
431 "<td width=3 class=tab></td>"
432 "<td><span class=action>%1 %2</span></td>"
433 "<td class=timestamp align=right>%3</td>"
434 "</tr>"
435 "</table>"
436 ).arg(
437 usernameSpan,
438 message,
439 timestamp()
440 )
441 );
442 }
443
appendNotification(const QString & message)444 void Chat::appendNotification(const QString &message)
445 {
446 QTextCursor cursor(doc);
447 cursor.movePosition(QTextCursor::End);
448
449 if(lastAppendedId != 0) {
450 appendSeparator(cursor);
451 lastAppendedId = 0;
452 }
453
454 cursor.insertHtml(QStringLiteral(
455 "<table width=\"100%\" class=notification><tr>"
456 "<td width=3 class=tab></td>"
457 "<td>%1</td>"
458 "<td align=right class=timestamp>%2</td>"
459 "</tr></table>"
460 ).arg(
461 htmlutils::newlineToBr(message),
462 timestamp()
463 )
464 );
465 }
466
userJoined(int id,const QString & name)467 void ChatWidget::userJoined(int id, const QString &name)
468 {
469 Q_UNUSED(name);
470
471 if(d->userlist) {
472 d->chats[0].doc->addResource(
473 QTextDocument::ImageResource,
474 QUrl(QStringLiteral("avatar://%1").arg(id)),
475 d->userlist->getUserById(id).avatar
476 );
477 if(d->chats.contains(id)) {
478 d->chats[id].doc->addResource(
479 QTextDocument::ImageResource,
480 QUrl(QStringLiteral("avatar://%1").arg(id)),
481 d->userlist->getUserById(id).avatar
482 );
483 }
484
485 } else {
486 qWarning("User #%d logged in, but userlist object not assigned to ChatWidget!", id);
487 }
488
489 // The server resends UserJoin messages during session reset.
490 // We don't need to see the join messages again.
491 if(d->announcedUsers.contains(id))
492 return;
493
494 d->announcedUsers << id;
495 const QString msg = tr("%1 joined the session").arg(d->usernameSpan(id));
496 const bool wasAtEnd = d->isAtEnd();
497
498 d->publicChat().appendNotification(msg);
499 if(wasAtEnd)
500 d->scrollToEnd(0);
501
502 if(d->chats.contains(id)) {
503 d->chats[id].appendNotification(msg);
504 if(wasAtEnd)
505 d->scrollToEnd(id);
506 }
507
508 notification::playSound(notification::Event::LOGIN);
509 }
510
userParted(int id)511 void ChatWidget::userParted(int id)
512 {
513 QString msg = tr("%1 left the session").arg(d->usernameSpan(id));
514 const bool wasAtEnd = d->isAtEnd();
515
516 d->publicChat().appendNotification(msg);
517 if(wasAtEnd)
518 d->scrollToEnd(0);
519
520 if(d->chats.contains(id)) {
521 d->chats[id].appendNotification(msg);
522 if(wasAtEnd)
523 d->scrollToEnd(id);
524 }
525
526 d->announcedUsers.removeAll(id);
527
528 notification::playSound(notification::Event::LOGOUT);
529 }
530
kicked(const QString & kickedBy)531 void ChatWidget::kicked(const QString &kickedBy)
532 {
533 const bool wasAtEnd = d->isAtEnd();
534 d->publicChat().appendNotification(tr("You have been kicked by %1").arg(kickedBy.toHtmlEscaped()));
535 if(wasAtEnd)
536 d->scrollToEnd(0);
537 }
538
receiveMessage(const protocol::MessagePtr & msg)539 void ChatWidget::receiveMessage(const protocol::MessagePtr &msg)
540 {
541 const bool wasAtEnd = d->isAtEnd();
542 int chatId = 0;
543
544 if(msg->type() == protocol::MSG_CHAT) {
545 const protocol::Chat &chat = msg.cast<protocol::Chat>();
546 const QString safetext = chat.message().toHtmlEscaped();
547
548 if(chat.isPin()) {
549 d->pinned->setPinText(safetext);
550 } else if(chat.isAction()) {
551 d->publicChat().appendAction(d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext));
552
553 } else {
554 if(d->compactMode)
555 d->publicChat().appendMessageCompact(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), chat.isShout());
556 else
557 d->publicChat().appendMessage(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), chat.isShout());
558 }
559
560 } else if(msg->type() == protocol::MSG_PRIVATE_CHAT) {
561 const protocol::PrivateChat &chat = msg.cast<protocol::PrivateChat>();
562 const QString safetext = chat.message().toHtmlEscaped();
563
564 if(chat.target() != d->myId && chat.contextId() != d->myId) {
565 qWarning("ChatWidget::recivePrivateMessage: message was targeted to user %d, but our ID is %d", chat.target(), d->myId);
566 return;
567 }
568
569 // The server echoes back the messages we send
570 chatId = chat.target() == d->myId ? chat.contextId() : chat.target();
571
572 if(!d->ensurePrivateChatExists(chatId, this))
573 return;
574
575 Chat &c = d->chats[chatId];
576
577 if(chat.isAction()) {
578 c.appendAction(d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext));
579
580 } else {
581 if(d->compactMode)
582 c.appendMessageCompact(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), false);
583 else
584 c.appendMessage(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), false);
585 }
586
587 } else {
588 qWarning("ChatWidget::receiveMessage: got wrong message type %s!", qPrintable(msg->messageName()));
589 return;
590 }
591
592 if(chatId != d->currentChat) {
593 for(int i=0;i<d->tabs->count();++i) {
594 if(d->tabs->tabData(i).toInt() == chatId) {
595 d->tabs->setTabTextColor(i, QColor(218, 68, 83));
596 break;
597 }
598 }
599 }
600
601 if(!d->myline->hasFocus() || chatId != d->currentChat)
602 notification::playSound(notification::Event::CHAT);
603
604 if(wasAtEnd)
605 d->scrollToEnd(chatId);
606 }
607
receiveMarker(int id,const QString & message)608 void ChatWidget::receiveMarker(int id, const QString &message)
609 {
610 const bool wasAtEnd = d->isAtEnd();
611
612 d->publicChat().appendNotification(QStringLiteral(
613 "<img src=\"theme:flag-red.svg\"> %1: %2"
614 ).arg(
615 d->usernameSpan(id),
616 htmlutils::linkify(message.toHtmlEscaped())
617 )
618 );
619
620 if(wasAtEnd)
621 d->scrollToEnd(0);
622 }
623
systemMessage(const QString & message,bool alert)624 void ChatWidget::systemMessage(const QString& message, bool alert)
625 {
626 Q_UNUSED(alert);
627 const bool wasAtEnd = d->isAtEnd();
628 d->publicChat().appendNotification(message.toHtmlEscaped());
629 if(wasAtEnd)
630 d->scrollToEnd(0);
631 }
632
sendMessage(const QString & msg)633 void ChatWidget::sendMessage(const QString &msg)
634 {
635 if(msg.at(0) == '/') {
636 // Special commands
637
638 int split = msg.indexOf(' ');
639 if(split<0)
640 split = msg.length();
641
642 const QString cmd = msg.mid(1, split-1).toLower();
643 const QString params = msg.mid(split).trimmed();
644
645 if(cmd == "clear") {
646 clear();
647 return;
648
649 } else if(cmd.at(0)=='!' && d->currentChat == 0) {
650 if(msg.length() > 2)
651 emit message(protocol::Chat::announce(d->myId, msg.mid(2)));
652 return;
653
654 } else if(cmd == "me") {
655 if(!params.isEmpty()) {
656 if(d->currentChat == 0)
657 emit message(protocol::Chat::action(d->myId, params, !d->preserveChat));
658 else
659 emit message(protocol::PrivateChat::action(d->myId, d->currentChat, params));
660 }
661 return;
662
663 } else if(cmd == "pin" && d->currentChat == 0) {
664 if(!params.isEmpty())
665 emit message(protocol::Chat::pin(d->myId, params));
666 return;
667
668 } else if(cmd == "unpin" && d->currentChat == 0) {
669 emit message(protocol::Chat::pin(d->myId, QStringLiteral("-")));
670 return;
671
672 } else if(cmd == "roll") {
673 utils::DiceRoll result = utils::diceRoll(params.isEmpty() ? QStringLiteral("1d6") : params);
674 if(result.number>0) {
675 if(d->currentChat == 0)
676 emit message(protocol::Chat::action(d->myId, "rolls " + result.toString(), !d->preserveChat));
677 else
678 emit message(protocol::PrivateChat::action(d->myId, d->currentChat, "rolls " + result.toString()));
679 } else
680 systemMessage(tr("Invalid dice roll description"));
681 return;
682
683 } else if(cmd == "help") {
684 const QString text = QStringLiteral(
685 "Available client commands:\n"
686 "/help - show this message\n"
687 "/clear - clear chat window\n"
688 "/! <text> - make an announcement (recorded in session history)\n"
689 "/me <text> - send action type message\n"
690 "/pin <text> - pin a message to the top of the chat box (Ops only)\n"
691 "/unpin - remove pinned message\n"
692 "/roll [AdX] - roll dice"
693 );
694 systemMessage(text);
695 return;
696 }
697
698 }
699
700 // A normal chat message
701 if(d->currentChat == 0)
702 emit message(protocol::Chat::regular(d->myId, msg, !d->preserveChat));
703 else
704 emit message(protocol::PrivateChat::regular(d->myId, d->currentChat, msg));
705 }
706
chatTabSelected(int index)707 void ChatWidget::chatTabSelected(int index)
708 {
709 d->chats[d->currentChat].scrollPosition = d->view->verticalScrollBar()->value();
710
711 const int id = d->tabs->tabData(index).toInt();
712 Q_ASSERT(d->chats.contains(id));
713 d->view->setDocument(d->chats[id].doc);
714 d->view->verticalScrollBar()->setValue(d->chats[id].scrollPosition);
715 d->tabs->setTabTextColor(index, QColor());
716 d->currentChat = d->tabs->tabData(index).toInt();
717 d->updatePreserveModeUi();
718 }
719
chatTabClosed(int index)720 void ChatWidget::chatTabClosed(int index)
721 {
722 const int id = d->tabs->tabData(index).toInt();
723 Q_ASSERT(d->chats.contains(id));
724 if(id == 0) {
725 // Can't close the public chat
726 return;
727 }
728
729 d->tabs->removeTab(index);
730
731 delete d->chats[id].doc;
732 d->chats.remove(id);
733 }
734
showChatContextMenu(const QPoint & pos)735 void ChatWidget::showChatContextMenu(const QPoint &pos)
736 {
737 auto menu = std::unique_ptr<QMenu>(d->view->createStandardContextMenu());
738
739 menu->addSeparator();
740
741 menu->addAction(tr("Clear"), this, &ChatWidget::clear);
742
743 auto compact = menu->addAction(tr("Compact mode"), this, &ChatWidget::setCompactMode);
744 compact->setCheckable(true);
745 compact->setChecked(d->compactMode);
746
747 if(d->isAttached) {
748 menu->addAction(tr("Detach"), this, &ChatWidget::detachRequested);
749 } else {
750 QWidget *win = parentWidget();
751 while(win->parent() != nullptr)
752 win = win->parentWidget();
753
754 menu->addAction(tr("Attach"), win, &QWidget::close);
755 }
756
757 menu->exec(d->view->mapToGlobal(pos));
758 }
759
setCompactMode(bool compact)760 void ChatWidget::setCompactMode(bool compact)
761 {
762 d->compactMode = compact;
763 QSettings().setValue("history/compactchat", compact);
764 }
765
766 }
767
768