1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/src/chatlog/chatlog.cpp

1395 lines
39 KiB
C++
Raw Normal View History

2014-11-16 19:58:43 +08:00
/*
Copyright © 2014-2019 by The qTox Project Contributors
2014-11-16 19:58:43 +08:00
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
2014-11-16 19:58:43 +08:00
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
2014-11-16 19:58:43 +08:00
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
2014-11-16 19:58:43 +08:00
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
2014-11-16 19:58:43 +08:00
*/
2014-11-12 21:11:25 +08:00
#include "chatlog.h"
#include "chatlinecontent.h"
#include "chatlinecontentproxy.h"
#include "chatmessage.h"
#include "content/filetransferwidget.h"
#include "content/text.h"
#include "src/widget/gui.h"
#include "src/widget/translator.h"
2019-02-19 02:53:43 +08:00
#include "src/widget/style.h"
#include "src/persistence/settings.h"
2014-11-16 19:40:44 +08:00
#include <QAction>
2014-11-12 21:11:25 +08:00
#include <QApplication>
#include <QClipboard>
#include <QDebug>
2015-01-20 17:44:05 +08:00
#include <QMouseEvent>
#include <QScrollBar>
#include <QShortcut>
#include <QTimer>
2014-11-12 21:11:25 +08:00
#include <algorithm>
#include <cassert>
2016-07-27 06:19:23 +08:00
namespace
{
template <class T>
2014-11-12 21:11:25 +08:00
T clamp(T x, T min, T max)
{
if (x > max)
2014-11-12 21:11:25 +08:00
return max;
if (x < min)
2014-11-12 21:11:25 +08:00
return min;
return x;
}
ChatLine::Ptr getChatMessageForIdx(ChatLogIdx idx,
const std::map<ChatLogIdx, ChatLine::Ptr>& messages)
{
auto existingMessageIt = messages.find(idx);
if (existingMessageIt == messages.end()) {
return ChatMessage::Ptr();
}
return existingMessageIt->second;
}
bool shouldRenderDate(ChatLogIdx idxToRender, const IChatLog& chatLog)
{
if (idxToRender == chatLog.getFirstIdx())
return true;
return chatLog.at(idxToRender - 1).getTimestamp().date()
!= chatLog.at(idxToRender).getTimestamp().date();
}
ChatMessage::Ptr dateMessageForItem(const ChatLogItem& item)
{
const auto& s = Settings::getInstance();
const auto date = item.getTimestamp().date();
auto dateText = date.toString(s.getDateFormat());
return ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime());
}
ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool colorizeNames,
const ChatLogMessage& chatLogMessage)
{
auto messageType = chatLogMessage.message.isAction ? ChatMessage::MessageType::ACTION
: ChatMessage::MessageType::NORMAL;
const bool bSelfMentioned =
std::any_of(chatLogMessage.message.metadata.begin(), chatLogMessage.message.metadata.end(),
[](const MessageMetadata& metadata) {
return metadata.type == MessageMetadataType::selfMention;
});
if (bSelfMentioned) {
messageType = ChatMessage::MessageType::ALERT;
}
const auto timestamp = chatLogMessage.message.timestamp;
return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType,
isSelf, chatLogMessage.state, timestamp, colorizeNames);
}
void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeNames,
const ChatLogMessage& chatLogMessage, ChatLine::Ptr& chatLine)
{
// HACK: This is kind of gross, but there's not an easy way to fit this into
// the existing architecture. This shouldn't ever fail since we should only
// correlate ChatMessages created here, however a logic bug could turn into
// a crash due to this dangerous cast. The alternative would be to make
// ChatLine a QObject which I didn't think was worth it.
auto chatMessage = static_cast<ChatMessage*>(chatLine.get());
if (chatMessage) {
if (chatLogMessage.state == MessageState::complete) {
chatMessage->markAsDelivered(chatLogMessage.message.timestamp);
} else if (chatLogMessage.state == MessageState::broken) {
chatMessage->markAsBroken();
}
} else {
chatLine = createMessage(displayName, isSelf, colorizeNames, chatLogMessage);
}
}
/**
* @return Chat message message type (info/warning) for the given system message
* @param[in] systemMessage
*/
ChatMessage::SystemMessageType getChatMessageType(const SystemMessage& systemMessage)
{
switch (systemMessage.messageType)
{
case SystemMessageType::fileSendFailed:
case SystemMessageType::messageSendFailed:
case SystemMessageType::unexpectedCallEnd:
return ChatMessage::ERROR;
case SystemMessageType::userJoinedGroup:
case SystemMessageType::userLeftGroup:
case SystemMessageType::peerNameChanged:
case SystemMessageType::peerStateChange:
case SystemMessageType::titleChanged:
case SystemMessageType::cleared:
case SystemMessageType::outgoingCall:
case SystemMessageType::incomingCall:
case SystemMessageType::callEnd:
return ChatMessage::INFO;
}
return ChatMessage::INFO;
}
ChatLogIdx firstItemAfterDate(QDate date, const IChatLog& chatLog)
{
auto idxs = chatLog.getDateIdxs(date, 1);
if (idxs.size()) {
return idxs[0].idx;
} else {
return chatLog.getNextIdx();
}
}
} // namespace
ChatLog::ChatLog(IChatLog& chatLog, const Core& core, QWidget* parent)
: QGraphicsView(parent)
, chatLog(chatLog)
, core(core)
2014-11-12 21:11:25 +08:00
{
2015-01-15 01:22:42 +08:00
// Create the scene
busyScene = new QGraphicsScene(this);
2014-11-12 21:11:25 +08:00
scene = new QGraphicsScene(this);
2015-02-01 00:39:25 +08:00
scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
2014-11-12 21:11:25 +08:00
setScene(scene);
busyNotification = ChatMessage::createBusyNotification();
busyNotification->addToScene(busyScene);
busyNotification->visibilityChanged(true);
2015-01-15 01:22:42 +08:00
// Cfg.
2014-11-12 21:11:25 +08:00
setInteractive(true);
2015-02-02 18:01:01 +08:00
setAcceptDrops(false);
2014-11-12 21:11:25 +08:00
setAlignment(Qt::AlignTop | Qt::AlignLeft);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setDragMode(QGraphicsView::NoDrag);
2015-02-02 18:01:01 +08:00
setViewportUpdateMode(MinimalViewportUpdate);
setContextMenuPolicy(Qt::CustomContextMenu);
2019-02-23 00:01:43 +08:00
setBackgroundBrush(QBrush(Style::getColor(Style::GroundBase), Qt::SolidPattern));
2014-11-12 21:11:25 +08:00
2015-01-15 01:22:42 +08:00
// The selection rect for multi-line selection
selGraphItem = scene->addRect(0, 0, 0, 0, selectionRectColor.darker(120), selectionRectColor);
2015-02-02 18:01:01 +08:00
selGraphItem->setZValue(-1.0); // behind all other items
2014-12-09 20:17:08 +08:00
2015-01-15 01:22:42 +08:00
// copy action (ie. Ctrl+C)
copyAction = new QAction(this);
copyAction->setIcon(QIcon::fromTheme("edit-copy"));
2014-11-12 21:11:25 +08:00
copyAction->setShortcut(QKeySequence::Copy);
copyAction->setEnabled(false);
connect(copyAction, &QAction::triggered, this, [this]() { copySelectedText(); });
2014-11-12 21:11:25 +08:00
addAction(copyAction);
// Ctrl+Insert shortcut
QShortcut* copyCtrlInsShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Insert), this);
connect(copyCtrlInsShortcut, &QShortcut::activated, this, [this]() { copySelectedText(); });
// select all action (ie. Ctrl+A)
selectAllAction = new QAction(this);
selectAllAction->setIcon(QIcon::fromTheme("edit-select-all"));
selectAllAction->setShortcut(QKeySequence::SelectAll);
connect(selectAllAction, &QAction::triggered, this, [this]() { selectAll(); });
addAction(selectAllAction);
2015-01-04 22:18:23 +08:00
2015-01-15 01:22:42 +08:00
// This timer is used to scroll the view while the user is
// moving the mouse past the top/bottom edge of the widget while selecting.
2015-01-04 22:18:23 +08:00
selectionTimer = new QTimer(this);
selectionTimer->setInterval(1000 / 30);
2015-01-04 22:18:23 +08:00
selectionTimer->setSingleShot(false);
selectionTimer->start();
connect(selectionTimer, &QTimer::timeout, this, &ChatLog::onSelectionTimerTimeout);
2015-01-07 20:05:28 +08:00
2015-01-15 01:22:42 +08:00
// Background worker
// Updates the layout of all chat-lines after a resize
workerTimer = new QTimer(this);
workerTimer->setSingleShot(false);
2015-02-01 01:37:20 +08:00
workerTimer->setInterval(5);
connect(workerTimer, &QTimer::timeout, this, &ChatLog::onWorkerTimeout);
2015-02-11 23:32:42 +08:00
// This timer is used to detect multiple clicks
multiClickTimer = new QTimer(this);
multiClickTimer->setSingleShot(true);
multiClickTimer->setInterval(QApplication::doubleClickInterval());
connect(multiClickTimer, &QTimer::timeout, this, &ChatLog::onMultiClickTimeout);
2015-02-11 23:32:42 +08:00
// selection
connect(this, &ChatLog::selectionChanged, this, [this]() {
copyAction->setEnabled(hasTextToBeCopied());
copySelectedText(true);
});
connect(&GUI::getInstance(), &GUI::themeReload, this, &ChatLog::reloadTheme);
reloadTheme();
retranslateUi();
Translator::registerHandler(std::bind(&ChatLog::retranslateUi, this), this);
connect(&chatLog, &IChatLog::itemUpdated, this, &ChatLog::renderMessage);
auto chatLogIdxRange = chatLog.getNextIdx() - chatLog.getFirstIdx();
auto firstChatLogIdx = (chatLogIdxRange < 100) ? chatLog.getFirstIdx() : chatLog.getNextIdx() - 100;
renderMessages(firstChatLogIdx, chatLog.getNextIdx());
2014-11-12 21:11:25 +08:00
}
2015-02-08 17:11:55 +08:00
ChatLog::~ChatLog()
{
Translator::unregister(this);
2015-02-10 00:48:54 +08:00
// Remove chatlines from scene
for (ChatLine::Ptr l : lines)
2015-02-10 00:48:54 +08:00
l->removeFromScene();
if (busyNotification)
2015-02-10 00:48:54 +08:00
busyNotification->removeFromScene();
if (typingNotification)
2015-02-10 00:48:54 +08:00
typingNotification->removeFromScene();
2015-02-08 17:11:55 +08:00
}
2014-11-12 21:11:25 +08:00
void ChatLog::clearSelection()
{
if (selectionMode == SelectionMode::None)
return;
for (int i = selFirstRow; i <= selLastRow; ++i)
2014-12-09 20:17:08 +08:00
lines[i]->selectionCleared();
2014-11-12 21:11:25 +08:00
2014-12-09 20:17:08 +08:00
selFirstRow = -1;
2014-11-12 21:11:25 +08:00
selLastRow = -1;
2014-12-09 20:17:08 +08:00
selClickedCol = -1;
selClickedRow = -1;
selectionMode = SelectionMode::None;
2015-02-11 23:32:42 +08:00
emit selectionChanged();
updateMultiSelectionRect();
2014-11-12 21:11:25 +08:00
}
QRect ChatLog::getVisibleRect() const
{
return mapToScene(viewport()->rect()).boundingRect().toRect();
2014-11-12 21:11:25 +08:00
}
void ChatLog::updateSceneRect()
{
setSceneRect(calculateSceneRect());
2014-11-12 21:11:25 +08:00
}
void ChatLog::layout(int start, int end, qreal width)
2014-11-12 21:11:25 +08:00
{
if (lines.empty())
return;
2014-11-12 21:11:25 +08:00
qreal h = 0.0;
2014-11-12 21:11:25 +08:00
2015-01-15 01:22:42 +08:00
// Line at start-1 is considered to have the correct position. All following lines are
// positioned in respect to this line.
if (start - 1 >= 0)
h = lines[start - 1]->sceneBoundingRect().bottom() + lineSpacing;
start = clamp<int>(start, 0, lines.size());
end = clamp<int>(end + 1, 0, lines.size());
2014-11-12 21:11:25 +08:00
for (int i = start; i < end; ++i) {
2015-01-04 20:29:14 +08:00
ChatLine* l = lines[i].get();
2014-11-12 21:11:25 +08:00
l->layout(width, QPointF(0.0, h));
h += l->sceneBoundingRect().height() + lineSpacing;
2014-11-12 21:11:25 +08:00
}
}
void ChatLog::mousePressEvent(QMouseEvent* ev)
{
QGraphicsView::mousePressEvent(ev);
if (ev->button() == Qt::LeftButton) {
2014-11-12 21:11:25 +08:00
clickPos = ev->pos();
clearSelection();
}
if (lastClickButton == ev->button()) {
// Counts only single clicks and first click of doule click
clickCount++;
}
else {
clickCount = 1; // restarting counter
lastClickButton = ev->button();
}
lastClickPos = ev->pos();
// Triggers on odd click counts
handleMultiClickEvent();
2014-11-12 21:11:25 +08:00
}
void ChatLog::mouseReleaseEvent(QMouseEvent* ev)
{
QGraphicsView::mouseReleaseEvent(ev);
selectionScrollDir = AutoScrollDirection::NoDirection;
multiClickTimer->start();
2014-11-12 21:11:25 +08:00
}
void ChatLog::mouseMoveEvent(QMouseEvent* ev)
{
QGraphicsView::mouseMoveEvent(ev);
QPointF scenePos = mapToScene(ev->pos());
if (ev->buttons() & Qt::LeftButton) {
// autoscroll
if (ev->pos().y() < 0)
selectionScrollDir = AutoScrollDirection::Up;
else if (ev->pos().y() > height())
selectionScrollDir = AutoScrollDirection::Down;
2015-01-04 22:18:23 +08:00
else
selectionScrollDir = AutoScrollDirection::NoDirection;
2015-01-04 22:18:23 +08:00
// select
if (selectionMode == SelectionMode::None
&& (clickPos - ev->pos()).manhattanLength() > QApplication::startDragDistance()) {
2014-11-12 21:11:25 +08:00
QPointF sceneClickPos = mapToScene(clickPos.toPoint());
ChatLine::Ptr line = findLineByPosY(scenePos.y());
2014-11-12 21:11:25 +08:00
ChatLineContent* content = getContentFromPos(sceneClickPos);
if (content) {
2014-12-09 20:17:08 +08:00
selClickedRow = content->getRow();
selClickedCol = content->getColumn();
selFirstRow = content->getRow();
selLastRow = content->getRow();
2014-11-12 21:11:25 +08:00
content->selectionStarted(sceneClickPos);
selectionMode = SelectionMode::Precise;
2014-11-12 21:11:25 +08:00
// ungrab mouse grabber
if (scene->mouseGrabberItem())
2014-11-12 21:11:25 +08:00
scene->mouseGrabberItem()->ungrabMouse();
} else if (line.get()) {
2015-01-20 17:31:50 +08:00
selClickedRow = line->getRow();
selFirstRow = selClickedRow;
selLastRow = selClickedRow;
selectionMode = SelectionMode::Multi;
}
2014-11-12 21:11:25 +08:00
}
if (selectionMode != SelectionMode::None) {
2015-01-02 18:25:07 +08:00
ChatLineContent* content = getContentFromPos(scenePos);
ChatLine::Ptr line = findLineByPosY(scenePos.y());
int row;
2014-11-12 21:11:25 +08:00
if (content) {
row = content->getRow();
2015-01-02 18:25:07 +08:00
int col = content->getColumn();
2014-11-12 21:11:25 +08:00
if (row == selClickedRow && col == selClickedCol) {
selectionMode = SelectionMode::Precise;
2014-11-12 21:11:25 +08:00
2015-01-02 18:25:07 +08:00
content->selectionMouseMove(scenePos);
selGraphItem->hide();
} else if (col != selClickedCol) {
selectionMode = SelectionMode::Multi;
2014-11-12 21:11:25 +08:00
2015-01-02 18:25:07 +08:00
lines[selClickedRow]->selectionCleared();
}
} else if (line.get()) {
2015-01-20 17:31:50 +08:00
row = line->getRow();
if (row != selClickedRow) {
selectionMode = SelectionMode::Multi;
lines[selClickedRow]->selectionCleared();
2015-01-02 18:25:07 +08:00
}
} else {
return;
}
if (row >= selClickedRow)
selLastRow = row;
if (row <= selClickedRow)
selFirstRow = row;
updateMultiSelectionRect();
2014-11-12 21:11:25 +08:00
}
2015-02-10 21:36:53 +08:00
2015-02-11 23:32:42 +08:00
emit selectionChanged();
}
2014-11-12 21:11:25 +08:00
}
// Much faster than QGraphicsScene::itemAt()!
2014-11-12 21:11:25 +08:00
ChatLineContent* ChatLog::getContentFromPos(QPointF scenePos) const
{
if (lines.empty())
2015-01-07 00:47:57 +08:00
return nullptr;
2014-11-12 21:11:25 +08:00
auto itr =
std::lower_bound(lines.cbegin(), lines.cend(), scenePos.y(), ChatLine::lessThanBSRectBottom);
2015-01-07 00:47:57 +08:00
// find content
if (itr != lines.cend() && (*itr)->sceneBoundingRect().contains(scenePos))
return (*itr)->getContent(scenePos);
2014-11-12 21:11:25 +08:00
return nullptr;
}
2015-01-14 06:59:38 +08:00
bool ChatLog::isOverSelection(QPointF scenePos) const
2014-11-12 21:11:25 +08:00
{
if (selectionMode == SelectionMode::Precise) {
2014-12-09 20:17:08 +08:00
ChatLineContent* content = getContentFromPos(scenePos);
2014-11-12 21:11:25 +08:00
if (content)
2014-12-09 20:17:08 +08:00
return content->isOverSelection(scenePos);
} else if (selectionMode == SelectionMode::Multi) {
if (selGraphItem->rect().contains(scenePos))
2014-12-09 20:17:08 +08:00
return true;
}
2014-11-12 21:11:25 +08:00
return false;
}
2015-01-14 06:59:38 +08:00
qreal ChatLog::useableWidth() const
2014-11-12 21:11:25 +08:00
{
return width() - verticalScrollBar()->sizeHint().width() - margins.right() - margins.left();
2014-11-12 21:11:25 +08:00
}
2015-01-05 21:06:14 +08:00
void ChatLog::reposition(int start, int end, qreal deltaY)
2014-11-12 21:11:25 +08:00
{
if (lines.isEmpty())
2014-11-12 21:11:25 +08:00
return;
start = clamp<int>(start, 0, lines.size() - 1);
end = clamp<int>(end + 1, 0, lines.size());
for (int i = start; i < end; ++i) {
2015-01-04 20:29:14 +08:00
ChatLine* l = lines[i].get();
2015-01-05 21:06:14 +08:00
l->moveBy(deltaY);
2014-11-12 21:11:25 +08:00
}
}
void ChatLog::insertChatlineAtBottom(ChatLine::Ptr l)
2014-11-12 21:11:25 +08:00
{
if (!l.get())
2015-01-05 01:21:35 +08:00
return;
2015-01-14 06:59:38 +08:00
bool stickToBtm = stickToBottom();
2014-11-12 21:11:25 +08:00
// insert
2015-01-20 17:31:50 +08:00
l->setRow(lines.size());
2015-01-14 06:59:38 +08:00
l->addToScene(scene);
2014-11-12 21:11:25 +08:00
lines.append(l);
// partial refresh
2015-01-20 17:31:50 +08:00
layout(lines.last()->getRow(), lines.size(), useableWidth());
2014-11-12 21:11:25 +08:00
updateSceneRect();
if (stickToBtm)
2014-11-12 21:11:25 +08:00
scrollToBottom();
checkVisibility();
updateTypingNotification();
2014-11-12 21:11:25 +08:00
}
void ChatLog::insertChatlineOnTop(ChatLine::Ptr l)
{
if (!l.get())
return;
insertChatlinesOnTop(QList<ChatLine::Ptr>() << l);
}
void ChatLog::insertChatlinesOnTop(const QList<ChatLine::Ptr>& newLines)
{
if (newLines.isEmpty())
return;
2015-02-10 21:30:49 +08:00
QGraphicsScene::ItemIndexMethod oldIndexMeth = scene->itemIndexMethod();
scene->setItemIndexMethod(QGraphicsScene::NoIndex);
// alloc space for old and new lines
QVector<ChatLine::Ptr> combLines;
combLines.reserve(newLines.size() + lines.size());
2015-02-10 21:30:49 +08:00
// add the new lines
int i = 0;
for (ChatLine::Ptr l : newLines) {
l->addToScene(scene);
2015-02-01 00:39:25 +08:00
l->visibilityChanged(false);
2015-02-10 21:30:49 +08:00
l->setRow(i++);
combLines.push_back(l);
}
// add the old lines
for (ChatLine::Ptr l : lines) {
2015-02-10 21:30:49 +08:00
l->setRow(i++);
combLines.push_back(l);
}
2015-02-10 21:30:49 +08:00
lines = combLines;
moveSelectionRectDownIfSelected(newLines.size());
2015-02-10 21:30:49 +08:00
scene->setItemIndexMethod(oldIndexMeth);
// redo layout
startResizeWorker();
}
2015-01-14 06:59:38 +08:00
bool ChatLog::stickToBottom() const
2014-11-12 21:11:25 +08:00
{
return verticalScrollBar()->value() == verticalScrollBar()->maximum();
}
void ChatLog::scrollToBottom()
{
2015-01-14 06:59:38 +08:00
updateSceneRect();
2014-11-12 21:11:25 +08:00
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatLog::startResizeWorker()
{
if (lines.empty())
return;
// (re)start the worker
if (!workerTimer->isActive()) {
// these values must not be reevaluated while the worker is running
workerStb = stickToBottom();
if (!visibleLines.empty())
workerAnchorLine = visibleLines.first();
}
// switch to busy scene displaying the busy notification if there is a lot
// of text to be resized
int txt = 0;
for (ChatLine::Ptr line : lines) {
2015-03-17 01:56:26 +08:00
if (txt > 500000)
break;
2015-03-17 01:56:26 +08:00
for (ChatLineContent* content : line->content)
txt += content->getText().size();
}
2015-03-17 01:56:26 +08:00
if (txt > 500000)
setScene(busyScene);
workerLastIndex = 0;
workerTimer->start();
verticalScrollBar()->hide();
}
void ChatLog::mouseDoubleClickEvent(QMouseEvent* ev)
2015-01-19 22:19:54 +08:00
{
QPointF scenePos = mapToScene(ev->pos());
ChatLineContent* content = getContentFromPos(scenePos);
if (content) {
2015-01-19 22:19:54 +08:00
content->selectionDoubleClick(scenePos);
selClickedCol = content->getColumn();
selClickedRow = content->getRow();
selFirstRow = content->getRow();
selLastRow = content->getRow();
selectionMode = SelectionMode::Precise;
2015-02-11 23:32:42 +08:00
emit selectionChanged();
2015-01-19 22:19:54 +08:00
}
if (lastClickButton == ev->button()) {
// Counts the second click of double click
clickCount++;
}
else {
clickCount = 1; // restarting counter
lastClickButton = ev->button();
}
lastClickPos = ev->pos();
// Triggers on even click counts
handleMultiClickEvent();
2015-01-19 22:19:54 +08:00
}
2014-11-12 21:11:25 +08:00
QString ChatLog::getSelectedText() const
{
if (selectionMode == SelectionMode::Precise) {
2014-12-09 20:17:08 +08:00
return lines[selClickedRow]->content[selClickedCol]->getSelectedText();
} else if (selectionMode == SelectionMode::Multi) {
2014-12-09 20:17:08 +08:00
// build a nicely formatted message
QString out;
2014-11-12 21:11:25 +08:00
for (int i = selFirstRow; i <= selLastRow; ++i) {
if (lines[i]->content[1]->getText().isEmpty())
continue;
QString timestamp = lines[i]->content[2]->getText().isEmpty()
? tr("pending")
: lines[i]->content[2]->getText();
2015-02-11 23:37:02 +08:00
QString author = lines[i]->content[0]->getText();
QString msg = lines[i]->content[1]->getText();
2014-11-12 21:11:25 +08:00
out +=
QString(out.isEmpty() ? "[%2] %1: %3" : "\n[%2] %1: %3").arg(author, timestamp, msg);
2014-12-09 20:17:08 +08:00
}
2014-11-12 21:11:25 +08:00
2014-12-09 20:17:08 +08:00
return out;
}
2014-11-12 21:11:25 +08:00
2014-12-09 20:17:08 +08:00
return QString();
2014-11-12 21:11:25 +08:00
}
2014-12-14 04:11:03 +08:00
bool ChatLog::isEmpty() const
{
return lines.isEmpty();
}
bool ChatLog::hasTextToBeCopied() const
2014-11-12 21:11:25 +08:00
{
return selectionMode != SelectionMode::None;
2014-11-12 21:11:25 +08:00
}
/**
* @brief Finds the chat line object at a position on screen
* @param pos Position on screen in global coordinates
* @sa getContentFromPos()
*/
ChatLineContent* ChatLog::getContentFromGlobalPos(QPoint pos) const
{
return getContentFromPos(mapToScene(mapFromGlobal(pos)));
}
2014-11-12 21:11:25 +08:00
void ChatLog::clear()
{
clearSelection();
QVector<ChatLine::Ptr> savedLines;
for (ChatLine::Ptr l : lines) {
if (isActiveFileTransfer(l))
savedLines.push_back(l);
else
l->removeFromScene();
}
2014-11-12 21:11:25 +08:00
lines.clear();
2015-01-04 20:29:14 +08:00
visibleLines.clear();
for (ChatLine::Ptr l : savedLines)
insertChatlineAtBottom(l);
2015-01-04 20:29:14 +08:00
2014-11-12 21:11:25 +08:00
updateSceneRect();
messages.clear();
2014-11-12 21:11:25 +08:00
}
void ChatLog::copySelectedText(bool toSelectionBuffer) const
2014-11-12 21:11:25 +08:00
{
QString text = getSelectedText();
QClipboard* clipboard = QApplication::clipboard();
if (clipboard && !text.isNull())
clipboard->setText(text, toSelectionBuffer ? QClipboard::Selection : QClipboard::Clipboard);
2014-11-12 21:11:25 +08:00
}
2015-01-10 18:57:46 +08:00
void ChatLog::setTypingNotificationVisible(bool visible)
{
if (typingNotification.get()) {
2015-01-10 18:57:46 +08:00
typingNotification->setVisible(visible);
updateTypingNotification();
}
}
void ChatLog::setTypingNotificationName(const QString& displayName)
{
if (!typingNotification.get()) {
setTypingNotification();
}
Text* text = static_cast<Text*>(typingNotification->getContent(1));
QString typingDiv = "<div class=typing>%1</div>";
text->setText(typingDiv.arg(tr("%1 is typing").arg(displayName)));
updateTypingNotification();
}
void ChatLog::scrollToLine(ChatLine::Ptr line)
{
if (!line.get())
return;
updateSceneRect();
verticalScrollBar()->setValue(line->sceneBoundingRect().top());
}
void ChatLog::selectAll()
{
if (lines.empty())
return;
clearSelection();
selectionMode = SelectionMode::Multi;
selFirstRow = 0;
selLastRow = lines.size() - 1;
2015-02-11 23:32:42 +08:00
emit selectionChanged();
updateMultiSelectionRect();
}
void ChatLog::fontChanged(const QFont& font)
{
for (ChatLine::Ptr l : lines) {
l->fontChanged(font);
}
}
2019-02-21 21:53:31 +08:00
void ChatLog::reloadTheme()
{
setStyleSheet(Style::getStylesheet("chatArea/chatArea.css"));
2019-02-23 00:01:43 +08:00
setBackgroundBrush(QBrush(Style::getColor(Style::GroundBase), Qt::SolidPattern));
selectionRectColor = Style::getColor(Style::SelectText);
selGraphItem->setBrush(QBrush(selectionRectColor));
selGraphItem->setPen(QPen(selectionRectColor.darker(120)));
setTypingNotification();
2019-02-21 21:53:31 +08:00
for (ChatLine::Ptr l : lines) {
l->reloadTheme();
}
}
void ChatLog::startSearch(const QString& phrase, const ParameterSearch& parameter)
{
disableSearchText();
bool bForwardSearch = false;
switch (parameter.period) {
case PeriodSearch::WithTheFirst: {
bForwardSearch = true;
searchPos.logIdx = chatLog.getFirstIdx();
searchPos.numMatches = 0;
break;
}
case PeriodSearch::WithTheEnd:
case PeriodSearch::None: {
bForwardSearch = false;
searchPos.logIdx = chatLog.getNextIdx();
searchPos.numMatches = 0;
break;
}
case PeriodSearch::AfterDate: {
bForwardSearch = true;
searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog);
searchPos.numMatches = 0;
break;
}
case PeriodSearch::BeforeDate: {
bForwardSearch = false;
searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog);
searchPos.numMatches = 0;
break;
}
}
if (bForwardSearch) {
onSearchDown(phrase, parameter);
} else {
onSearchUp(phrase, parameter);
}
}
void ChatLog::onSearchUp(const QString& phrase, const ParameterSearch& parameter)
{
auto result = chatLog.searchBackward(searchPos, phrase, parameter);
handleSearchResult(result, SearchDirection::Up);
}
void ChatLog::onSearchDown(const QString& phrase, const ParameterSearch& parameter)
{
auto result = chatLog.searchForward(searchPos, phrase, parameter);
handleSearchResult(result, SearchDirection::Down);
}
void ChatLog::handleSearchResult(SearchResult result, SearchDirection direction)
{
if (!result.found) {
emit messageNotFoundShow(direction);
return;
}
disableSearchText();
searchPos = result.pos;
auto const firstRenderedIdx = (messages.empty()) ? chatLog.getNextIdx() : messages.begin()->first;
renderMessages(searchPos.logIdx, firstRenderedIdx, [this, result] {
auto msg = messages.at(searchPos.logIdx);
scrollToLine(msg);
auto text = qobject_cast<Text*>(msg->getContent(1));
text->selectText(result.exp, std::make_pair(result.start, result.len));
});
}
void ChatLog::forceRelayout()
{
startResizeWorker();
}
void ChatLog::checkVisibility()
2014-11-12 21:11:25 +08:00
{
if (lines.empty())
2015-01-04 04:19:52 +08:00
return;
2015-01-14 17:34:52 +08:00
// find first visible line
auto lowerBound = std::lower_bound(lines.cbegin(), lines.cend(), getVisibleRect().top(),
ChatLine::lessThanBSRectBottom);
2014-11-12 21:11:25 +08:00
2015-01-14 17:34:52 +08:00
// find last visible line
auto upperBound = std::lower_bound(lowerBound, lines.cend(), getVisibleRect().bottom(),
ChatLine::lessThanBSRectTop);
2014-11-12 21:11:25 +08:00
const ChatLine::Ptr lastLineBeforeVisible = lowerBound == lines.cbegin()
? ChatLine::Ptr()
: *std::prev(lowerBound);
2014-11-12 21:11:25 +08:00
// set visibilty
2015-01-04 20:29:14 +08:00
QList<ChatLine::Ptr> newVisibleLines;
for (auto itr = lowerBound; itr != upperBound; ++itr) {
2014-11-12 21:11:25 +08:00
newVisibleLines.append(*itr);
if (!visibleLines.contains(*itr))
2014-11-12 21:11:25 +08:00
(*itr)->visibilityChanged(true);
visibleLines.removeOne(*itr);
}
2015-01-14 17:34:52 +08:00
// these lines are no longer visible
for (ChatLine::Ptr line : visibleLines)
2014-11-12 21:11:25 +08:00
line->visibilityChanged(false);
visibleLines = newVisibleLines;
// enforce order
std::sort(visibleLines.begin(), visibleLines.end(), ChatLine::lessThanRowIndex);
2014-11-12 21:11:25 +08:00
// if (!visibleLines.empty())
// qDebug() << "visible from " << visibleLines.first()->getRow() << "to " <<
// visibleLines.last()->getRow() << " total " << visibleLines.size();
2019-01-28 00:36:20 +08:00
if (!visibleLines.isEmpty()) {
emit firstVisibleLineChanged(lastLineBeforeVisible, visibleLines.at(0));
2019-01-28 00:36:20 +08:00
}
2014-11-12 21:11:25 +08:00
}
void ChatLog::scrollContentsBy(int dx, int dy)
{
QGraphicsView::scrollContentsBy(dx, dy);
checkVisibility();
2014-11-12 21:11:25 +08:00
}
void ChatLog::resizeEvent(QResizeEvent* ev)
{
bool stb = stickToBottom();
if (ev->size().width() != ev->oldSize().width()) {
startResizeWorker();
stb = false; // let the resize worker handle it
}
2014-11-12 21:11:25 +08:00
QGraphicsView::resizeEvent(ev);
if (stb)
scrollToBottom();
updateBusyNotification();
}
void ChatLog::updateMultiSelectionRect()
{
if (selectionMode == SelectionMode::Multi && selFirstRow >= 0 && selLastRow >= 0) {
QRectF selBBox;
selBBox = selBBox.united(lines[selFirstRow]->sceneBoundingRect());
selBBox = selBBox.united(lines[selLastRow]->sceneBoundingRect());
if (selGraphItem->rect() != selBBox)
2015-02-02 18:01:01 +08:00
scene->invalidate(selGraphItem->rect());
2015-02-01 02:19:13 +08:00
selGraphItem->setRect(selBBox);
selGraphItem->show();
} else {
selGraphItem->hide();
}
2014-11-12 21:11:25 +08:00
}
2015-01-04 22:18:23 +08:00
2015-01-10 18:57:46 +08:00
void ChatLog::updateTypingNotification()
{
ChatLine* notification = typingNotification.get();
if (!notification)
2015-01-10 18:57:46 +08:00
return;
qreal posY = 0.0;
if (!lines.empty())
posY = lines.last()->sceneBoundingRect().bottom() + lineSpacing;
2015-01-10 18:57:46 +08:00
notification->layout(useableWidth(), QPointF(0.0, posY));
}
void ChatLog::updateBusyNotification()
{
// repoisition the busy notification (centered)
busyNotification->layout(useableWidth(), getVisibleRect().topLeft()
+ QPointF(0, getVisibleRect().height() / 2.0));
}
ChatLine::Ptr ChatLog::findLineByPosY(qreal yPos) const
{
auto itr = std::lower_bound(lines.cbegin(), lines.cend(), yPos, ChatLine::lessThanBSRectBottom);
if (itr != lines.cend())
return *itr;
return ChatLine::Ptr();
}
QRectF ChatLog::calculateSceneRect() const
{
qreal bottom = (lines.empty() ? 0.0 : lines.last()->sceneBoundingRect().bottom());
if (typingNotification.get() != nullptr)
bottom += typingNotification->sceneBoundingRect().height() + lineSpacing;
return QRectF(-margins.left(), -margins.top(), useableWidth(),
bottom + margins.bottom() + margins.top());
}
2015-01-04 22:18:23 +08:00
void ChatLog::onSelectionTimerTimeout()
{
const int scrollSpeed = 10;
switch (selectionScrollDir) {
case AutoScrollDirection::Up:
2015-01-04 22:18:23 +08:00
verticalScrollBar()->setValue(verticalScrollBar()->value() - scrollSpeed);
break;
case AutoScrollDirection::Down:
2015-01-04 22:18:23 +08:00
verticalScrollBar()->setValue(verticalScrollBar()->value() + scrollSpeed);
break;
default:
break;
}
}
void ChatLog::onWorkerTimeout()
{
// Fairly arbitrary but
// large values will make the UI unresponsive
2015-02-01 01:37:20 +08:00
const int stepSize = 50;
layout(workerLastIndex, workerLastIndex + stepSize, useableWidth());
workerLastIndex += stepSize;
// done?
if (workerLastIndex >= lines.size()) {
workerTimer->stop();
// switch back to the scene containing the chat messages
setScene(scene);
// make sure everything gets updated
updateSceneRect();
checkVisibility();
updateTypingNotification();
updateMultiSelectionRect();
// scroll
if (workerStb)
scrollToBottom();
else
scrollToLine(workerAnchorLine);
// don't keep a Ptr to the anchor line
workerAnchorLine = ChatLine::Ptr();
// hidden during busy screen
verticalScrollBar()->show();
2018-02-10 23:50:48 +08:00
emit workerTimeoutFinished();
}
}
2015-01-27 16:58:08 +08:00
void ChatLog::onMultiClickTimeout()
{
clickCount = 0;
}
void ChatLog::renderMessage(ChatLogIdx idx)
{
renderMessages(idx, idx + 1);
}
void ChatLog::renderMessages(ChatLogIdx begin, ChatLogIdx end,
std::function<void(void)> onCompletion)
{
QList<ChatLine::Ptr> beforeLines;
QList<ChatLine::Ptr> afterLines;
for (auto i = begin; i < end; ++i) {
auto chatMessage = getChatMessageForIdx(i, messages);
renderItem(chatLog.at(i), needsToHideName(i), colorizeNames, chatMessage);
if (messages.find(i) == messages.end()) {
QList<ChatLine::Ptr>* lines =
(messages.empty() || i > messages.rbegin()->first) ? &afterLines : &beforeLines;
messages.insert({i, chatMessage});
if (shouldRenderDate(i, chatLog)) {
lines->push_back(dateMessageForItem(chatLog.at(i)));
}
lines->push_back(chatMessage);
}
}
for (auto const& line : afterLines) {
insertChatlineAtBottom(line);
}
if (!beforeLines.empty()) {
// Rendering upwards is expensive and has async behavior for chatWidget.
// Once rendering completes we call our completion callback once and
// then disconnect the signal
if (onCompletion) {
auto connection = std::make_shared<QMetaObject::Connection>();
*connection = connect(this, &ChatLog::workerTimeoutFinished,
[this, onCompletion, connection] {
onCompletion();
this->disconnect(*connection);
});
}
insertChatlinesOnTop(beforeLines);
} else if (onCompletion) {
onCompletion();
}
}
void ChatLog::handleMultiClickEvent()
{
// Ignore single or double clicks
if (clickCount < 2)
return;
switch (clickCount) {
2017-06-01 15:50:59 +08:00
case 3:
QPointF scenePos = mapToScene(lastClickPos);
ChatLineContent* content = getContentFromPos(scenePos);
2017-06-01 15:50:59 +08:00
if (content) {
content->selectionTripleClick(scenePos);
selClickedCol = content->getColumn();
selClickedRow = content->getRow();
selFirstRow = content->getRow();
selLastRow = content->getRow();
selectionMode = SelectionMode::Precise;
2017-06-01 15:50:59 +08:00
emit selectionChanged();
}
break;
}
}
void ChatLog::showEvent(QShowEvent*)
2015-01-27 16:58:08 +08:00
{
// Empty.
// The default implementation calls centerOn - for some reason - causing
// the scrollbar to move.
}
void ChatLog::focusInEvent(QFocusEvent* ev)
{
QGraphicsView::focusInEvent(ev);
if (selectionMode != SelectionMode::None) {
selGraphItem->setBrush(QBrush(selectionRectColor));
for (int i = selFirstRow; i <= selLastRow; ++i)
lines[i]->selectionFocusChanged(true);
}
}
void ChatLog::focusOutEvent(QFocusEvent* ev)
{
QGraphicsView::focusOutEvent(ev);
if (selectionMode != SelectionMode::None) {
selGraphItem->setBrush(QBrush(selectionRectColor.lighter(120)));
for (int i = selFirstRow; i <= selLastRow; ++i)
lines[i]->selectionFocusChanged(false);
}
}
void ChatLog::wheelEvent(QWheelEvent *event)
{
QGraphicsView::wheelEvent(event);
checkVisibility();
}
void ChatLog::retranslateUi()
{
copyAction->setText(tr("Copy"));
selectAllAction->setText(tr("Select all"));
}
bool ChatLog::isActiveFileTransfer(ChatLine::Ptr l)
{
int count = l->getColumnCount();
for (int i = 0; i < count; ++i) {
ChatLineContent* content = l->getContent(i);
ChatLineContentProxy* proxy = qobject_cast<ChatLineContentProxy*>(content);
if (!proxy)
continue;
2017-01-06 19:02:54 +08:00
QWidget* widget = proxy->getWidget();
FileTransferWidget* transferWidget = qobject_cast<FileTransferWidget*>(widget);
if (transferWidget && transferWidget->isActive())
return true;
}
return false;
}
/**
* @brief Adjusts the selection based on chatlog changing lines
* @param offset Amount to shift selection rect up by. Must be non-negative.
*/
void ChatLog::moveSelectionRectUpIfSelected(int offset)
{
assert(offset >= 0);
switch (selectionMode)
{
case SelectionMode::None:
return;
case SelectionMode::Precise:
movePreciseSelectionUp(offset);
break;
case SelectionMode::Multi:
moveMultiSelectionUp(offset);
break;
}
}
/**
* @brief Adjusts the selections based on chatlog changing lines
* @param offset removed from the lines indexes. Must be non-negative.
*/
void ChatLog::moveSelectionRectDownIfSelected(int offset)
{
assert(offset >= 0);
switch (selectionMode)
{
case SelectionMode::None:
return;
case SelectionMode::Precise:
movePreciseSelectionDown(offset);
break;
case SelectionMode::Multi:
moveMultiSelectionDown(offset);
break;
}
}
void ChatLog::movePreciseSelectionDown(int offset)
{
assert(selFirstRow == selLastRow && selFirstRow == selClickedRow);
const int lastLine = lines.size() - 1;
if (selClickedRow + offset > lastLine) {
clearSelection();
} else {
const int newRow = selClickedRow + offset;
selClickedRow = newRow;
selLastRow = newRow;
selFirstRow = newRow;
emit selectionChanged();
}
}
void ChatLog::movePreciseSelectionUp(int offset)
{
assert(selFirstRow == selLastRow && selFirstRow == selClickedRow);
if (selClickedRow < offset) {
clearSelection();
} else {
const int newRow = selClickedRow - offset;
selClickedRow = newRow;
selLastRow = newRow;
selFirstRow = newRow;
emit selectionChanged();
}
}
void ChatLog::moveMultiSelectionUp(int offset)
{
if (selLastRow < offset) { // entire selection now out of bounds
clearSelection();
} else {
selLastRow -= offset;
selClickedRow = std::max(0, selClickedRow - offset);
selFirstRow = std::max(0, selFirstRow - offset);
updateMultiSelectionRect();
emit selectionChanged();
}
}
void ChatLog::moveMultiSelectionDown(int offset)
{
const int lastLine = lines.size() - 1;
if (selFirstRow + offset > lastLine) { // entire selection now out of bounds
clearSelection();
} else {
selFirstRow += offset;
selClickedRow = std::min(lastLine, selClickedRow + offset);
selLastRow = std::min(lastLine, selLastRow + offset);
updateMultiSelectionRect();
emit selectionChanged();
}
}
void ChatLog::setTypingNotification()
{
typingNotification = ChatMessage::createTypingNotification();
typingNotification->visibilityChanged(true);
typingNotification->setVisible(false);
typingNotification->addToScene(scene);
updateTypingNotification();
}
void ChatLog::renderItem(const ChatLogItem& item, bool hideName, bool colorizeNames, ChatLine::Ptr& chatMessage)
{
const auto& sender = item.getSender();
bool isSelf = sender == core.getSelfId().getPublicKey();
switch (item.getContentType()) {
case ChatLogItem::ContentType::message: {
const auto& chatLogMessage = item.getContentAsMessage();
renderMessageRaw(item.getDisplayName(), isSelf, colorizeNames, chatLogMessage, chatMessage);
break;
}
case ChatLogItem::ContentType::fileTransfer: {
const auto& file = item.getContentAsFile();
renderFile(item.getDisplayName(), file.file, isSelf, item.getTimestamp(), chatMessage);
break;
}
case ChatLogItem::ContentType::systemMessage: {
const auto& systemMessage = item.getContentAsSystemMessage();
auto chatMessageType = getChatMessageType(systemMessage);
chatMessage = ChatMessage::createChatInfoMessage(systemMessage.toString(), chatMessageType, QDateTime::currentDateTime());
// Ignore caller's decision to hide the name. We show the icon in the
// slot of the sender's name so we always want it visible
hideName = false;
break;
}
}
if (hideName) {
chatMessage->getContent(0)->hide();
}
}
void ChatLog::renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp,
ChatLine::Ptr& chatMessage)
{
if (!chatMessage) {
CoreFile* coreFile = core.getCoreFile();
assert(coreFile);
chatMessage = ChatMessage::createFileTransferMessage(displayName, *coreFile, file, isSelf, timestamp);
} else {
auto proxy = static_cast<ChatLineContentProxy*>(chatMessage->getContent(1));
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
auto ftWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
ftWidget->onFileTransferUpdate(file);
}
}
/**
* @brief Show, is it needed to hide message author name or not
* @param idx ChatLogIdx of the message
* @return True if the name should be hidden, false otherwise
*/
bool ChatLog::needsToHideName(ChatLogIdx idx) const
{
// If the previous message is not rendered we should show the name
// regardless of other constraints
auto itemBefore = messages.find(idx - 1);
if (itemBefore == messages.end()) {
return false;
}
const auto& prevItem = chatLog.at(idx - 1);
const auto& currentItem = chatLog.at(idx);
// Always show the * in the name field for action messages
if (currentItem.getContentType() == ChatLogItem::ContentType::message
&& currentItem.getContentAsMessage().message.isAction) {
return false;
}
qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp());
return currentItem.getSender() == prevItem.getSender()
&& messagesTimeDiff < repNameAfter;
}
void ChatLog::disableSearchText()
{
auto msgIt = messages.find(searchPos.logIdx);
if (msgIt != messages.end()) {
auto text = qobject_cast<Text*>(msgIt->second->getContent(1));
text->deselectText();
}
}
void ChatLog::removeSearchPhrase()
{
disableSearchText();
}
void ChatLog::jumpToDate(QDate date) {
auto idx = firstItemAfterDate(date, chatLog);
jumpToIdx(idx);
}
void ChatLog::jumpToIdx(ChatLogIdx idx) {
if (messages.find(idx) == messages.end()) {
renderMessages(idx, chatLog.getNextIdx());
}
scrollToLine(messages[idx]);
}