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

520 lines
13 KiB
C++
Raw Normal View History

2014-11-12 21:11:25 +08:00
#include "chatlog.h"
#include "chatline.h"
#include "chatlinecontent.h"
#include "chatlinecontentproxy.h"
#include "content/text.h"
#include "content/filetransferwidget.h"
#include "content/spinner.h"
#include <QDebug>
#include <QScrollBar>
#include <QApplication>
#include <QMenu>
#include <QClipboard>
template<class T>
T clamp(T x, T min, T max)
{
if(x > max)
return max;
if(x < min)
return min;
return x;
}
ChatLog::ChatLog(QWidget* parent)
: QGraphicsView(parent)
{
scene = new QGraphicsScene(this);
scene->setItemIndexMethod(QGraphicsScene::NoIndex);
setScene(scene);
setInteractive(true);
setAlignment(Qt::AlignTop | Qt::AlignLeft);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setDragMode(QGraphicsView::NoDrag);
setViewportUpdateMode(SmartViewportUpdate);
//setRenderHint(QPainter::TextAntialiasing);
// copy action
copyAction = new QAction(this);
copyAction->setShortcut(QKeySequence::Copy);
addAction(copyAction);
connect(copyAction, &QAction::triggered, this, [ = ](bool)
{
copySelectedText();
});
}
ChatLog::~ChatLog()
{
for(ChatLine* line : lines)
delete line;
}
void ChatLog::addTextLine(const QString& sender, const QString& text, QDateTime timestamp)
{
ChatLine* line = new ChatLine(scene);
line->addColumn(new Text(sender, true), ColumnFormat(75.0, ColumnFormat::FixedSize, 1, ColumnFormat::Right));
line->addColumn(new Text(text), ColumnFormat(1.0, ColumnFormat::VariableSize));
line->addColumn(new Text(timestamp.toString("hh:mm")), ColumnFormat(50.0, ColumnFormat::FixedSize, 1));
insertChatline(line);
}
void ChatLog::addWidgetLine(const QString& sender, QDateTime timestamp)
{
ChatLine* line = new ChatLine(scene);
line->addColumn(new Text(sender, true), ColumnFormat(75.0, ColumnFormat::FixedSize, 1));
line->addColumn(new ChatLineContentProxy(new FileTransferWidget()), ColumnFormat(1.0, ColumnFormat::VariableSize));
line->addColumn(new Spinner(QSizeF(16, 16)), ColumnFormat(50.0, ColumnFormat::FixedSize, 1, ColumnFormat::Right));
insertChatline(line);
}
void ChatLog::clearSelection()
{
if(selStartRow >= 0)
for(int r = qMin(selStartRow, selLastRow); r <= qMax(selLastRow, selStartRow) && r < lines.size(); ++r)
lines[r]->selectionCleared();
selStartRow = -1;
selStartCol = -1;
selLastRow = -1;
selLastCol = -1;
}
void ChatLog::dbgPopulate()
{
for(int i = 0; i < 2000; i++)
addTextLine("Jemp longlong name foo moth", "Line " + QString::number(i) +
" Lorem ipsum <b>Hello</b> dolor sit amet, "
"consectetur adipisicing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna "
"aliqua. Ut enim ad minim veniam, quis nostrud "
"exercitation ullamco laboris nisi ut aliquip ex "
"ea commodo consequat. ");
}
QRect ChatLog::getVisibleRect() const
{
return mapToScene(viewport()->rect()).boundingRect().toRect();
}
void ChatLog::updateSceneRect()
{
setSceneRect(QRectF(0, 0, width(), lines.empty() ? 0.0 : lines.last()->boundingSceneRect().bottom()));
}
bool ChatLog::layout(int start, int end, qreal width)
{
//qDebug() << "layout " << start << end;
if(lines.empty())
return false;
start = clamp<int>(start, 0, lines.size() - 1);
end = clamp<int>(end + 1, 0, lines.size());
qreal h = lines[start]->boundingSceneRect().top();
bool needsReposition = false;
for(int i = start; i < end; ++i)
{
ChatLine* l = lines[i];
qreal oldHeight = l->boundingSceneRect().height();
l->layout(width, QPointF(0, h));
if(oldHeight != l->boundingSceneRect().height())
needsReposition = true;
h += l->boundingSceneRect().height() + lineSpacing;
}
// move up
if(needsReposition)
reposition(end-1, end+10);
return needsReposition;
}
void ChatLog::partialUpdate()
{
checkVisibility();
if(visibleLines.empty())
return;
auto oldUpdateMode = viewportUpdateMode();
setViewportUpdateMode(NoViewportUpdate);
static int count = 0;
int count2 = 0;
int lastNonDirty = visibleLines.first()->getRowIndex();
bool repos;
do
{
repos = false;
if(!visibleLines.empty())
{
repos = layout(visibleLines.first()->getRowIndex(), visibleLines.last()->getRowIndex(), useableWidth());
lastNonDirty = visibleLines.last()->getRowIndex();
}
checkVisibility();
count2++;
}
while(repos);
reposition(visibleLines.last()->getRowIndex(), lines.size());
checkVisibility();
count = qMax(count, count2);
//qDebug() << "COUNT: " << count;
setViewportUpdateMode(oldUpdateMode);
updateSceneRect();
}
void ChatLog::fullUpdate()
{
layout(0, lines.size(), useableWidth());
checkVisibility();
updateSceneRect();
}
void ChatLog::mousePressEvent(QMouseEvent* ev)
{
QGraphicsView::mousePressEvent(ev);
QPointF scenePos = mapToScene(ev->pos());
if(ev->button() == Qt::LeftButton)
{
clickPos = ev->pos();
clearSelection();
}
if(ev->button() == Qt::RightButton)
{
if(!isOverSelection(scenePos))
clearSelection();
showContextMenu(ev->globalPos(), scenePos);
}
}
void ChatLog::mouseReleaseEvent(QMouseEvent* ev)
{
QGraphicsView::mouseReleaseEvent(ev);
if(ev->button() == Qt::LeftButton)
selecting = false;
}
void ChatLog::mouseMoveEvent(QMouseEvent* ev)
{
QGraphicsView::mouseMoveEvent(ev);
QPointF scenePos = mapToScene(ev->pos());
if(ev->buttons() & Qt::LeftButton)
{
if(!selecting && (clickPos - ev->pos()).manhattanLength() > QApplication::startDragDistance())
{
QPointF sceneClickPos = mapToScene(clickPos.toPoint());
ChatLineContent* content = getContentFromPos(sceneClickPos);
if(content)
{
selStartRow = content->getRow();
selStartCol = content->getColumn();
selLastRow = selStartRow;
selLastCol = selStartCol;
content->selectionStarted(sceneClickPos);
selecting = true;
// ungrab mouse grabber
if(scene->mouseGrabberItem())
scene->mouseGrabberItem()->ungrabMouse();
}
}
}
if(selecting && ev->pos() != lastPos)
{
lastPos = ev->pos();
ChatLineContent* content = getContentFromPos(scenePos);
if(content)
{
// TODO: turn this into a sane algo.
int row = content->getRow();
int col = content->getColumn();
int firstRow = selStartRow;
// selection
for(int r = qMin(firstRow, row + 1); r < qMax(row, firstRow); r++)
lines[r]->selectAll();
if(row != selStartRow)
for(int c = 0; c < col; c++)
lines[selStartRow]->selectAll();
if(row == selStartRow)
content->selectionMouseMove(scenePos);
else
{
lines[row]->selectAll();
selStartCol = 0;
selLastCol = lines[row]->getColumnCount();
}
// de-selection
if(row < selStartRow)
selLastRow = qMin(row, selLastRow);
else
selLastRow = qMax(row, selLastRow);
if(col < selStartCol)
selLastCol = qMin(col, selLastCol);
else
selLastCol = qMax(col, selLastCol);
for(int r = qMin(row, selLastRow); r < qMax(row, selLastRow + 1) && r < lines.size(); ++r)
if(r != row)
lines[r]->selectionCleared();
if(row == selStartRow)
for(int c = col + 1; c < lines[row]->getColumnCount() && c < selLastCol; ++c)
lines[row]->selectionCleared(c);
}
}
}
ChatLineContent* ChatLog::getContentFromPos(QPointF scenePos) const
{
QGraphicsItem* item = scene->itemAt(scenePos, QTransform());
if(item && item->type() == ChatLineContent::ChatLineContentType)
return static_cast<ChatLineContent*>(item);
return nullptr;
}
bool ChatLog::isOverSelection(QPointF scenePos)
{
ChatLineContent* content = getContentFromPos(scenePos);
if(content)
return content->isOverSelection(scenePos);
return false;
}
int ChatLog::useableWidth()
{
return width() - verticalScrollBar()->sizeHint().width();
}
void ChatLog::reposition(int start, int end)
{
if(lines.isEmpty())
return;
start = clamp<int>(start, 0, lines.size() - 1);
end = clamp<int>(end + 1, 0, lines.size());
qreal h = lines[start]->boundingSceneRect().bottom() + lineSpacing;
for(int i = start + 1; i < end; ++i)
{
ChatLine* l = lines[i];
l->layout(QPointF(0, h));
h += l->boundingSceneRect().height() + lineSpacing;
}
}
void ChatLog::repositionDownTo(int start, qreal end)
{
if(lines.isEmpty())
return;
start = clamp<int>(start, 0, lines.size() - 1);
qreal h = lines[start]->boundingSceneRect().bottom() + lineSpacing;
for(int i = start + 1; i < lines.size(); ++i)
{
ChatLine* l = lines[i];
l->layout(QPointF(0, h));
h += l->boundingSceneRect().height() + lineSpacing;
if(h > end)
break;
}
}
void ChatLog::insertChatline(ChatLine* l)
{
stickToBtm = stickToBottom();
l->setRowIndex(lines.size());
lines.append(l);
layout(lines.last()->getRowIndex() - 1, lines.size(), useableWidth());
updateSceneRect();
if(stickToBtm)
scrollToBottom();
checkVisibility();
}
bool ChatLog::stickToBottom()
{
return verticalScrollBar()->value() == verticalScrollBar()->maximum();
}
void ChatLog::scrollToBottom()
{
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
updateGeometry();
checkVisibility();
}
QString ChatLog::getSelectedText() const
{
QString ret;
const int rowStart = qMin(selStartRow, selLastRow);
const int rowLast = qMax(selStartRow, selLastRow);
const int colStart = qMin(selStartCol, selLastCol);
const int colLast = qMax(selStartCol, selLastCol);
for(int r = rowStart; r <= rowLast && r < lines.size(); ++r)
for(int c = colStart; c <= colLast && c < lines[r]->getColumnCount(); ++c)
ret.append(lines[r]->content[c]->getSelectedText() + '\n');
return ret;
}
void ChatLog::showContextMenu(const QPoint& globalPos, const QPointF& scenePos)
{
QMenu menu;
// populate
QAction* copyAction = menu.addAction(QIcon::fromTheme("edit-copy"), "Copy");
menu.addSeparator();
QAction* clearAction = menu.addAction("Clear log");
if(!isOverSelection(scenePos))
copyAction->setDisabled(true);
// show
QAction* action = menu.exec(globalPos);
if(action == copyAction)
copySelectedText();
if(action == clearAction)
clear();
}
void ChatLog::clear()
{
visibleLines.clear();
clearSelection();
for(ChatLine* line : lines)
delete line;
lines.clear();
updateSceneRect();
}
void ChatLog::copySelectedText() const
{
QString text = getSelectedText();
QClipboard* clipboard = QApplication::clipboard();
clipboard->setText(text);
}
void ChatLog::checkVisibility()
{
// find first visible row
QList<ChatLine*>::const_iterator upperBound;
upperBound = std::upper_bound(lines.cbegin(), lines.cend(), getVisibleRect().top(), [](const qreal lhs, const ChatLine* rhs)
{
return lhs < rhs->boundingSceneRect().bottom();
});
if(upperBound == lines.end())
upperBound = lines.begin();
// find last visible row
QList<ChatLine*>::const_iterator lowerBound;
lowerBound = std::lower_bound(lines.cbegin(), lines.cend(), getVisibleRect().bottom(), [](const ChatLine* lhs, const qreal rhs)
{
return lhs->boundingSceneRect().bottom() < rhs;
});
if(lowerBound == lines.end())
lowerBound = lines.end();
// set visibilty
QList<ChatLine*> newVisibleLines;
for(auto itr = upperBound; itr <= lowerBound && itr != lines.end(); ++itr)
{
newVisibleLines.append(*itr);
if(!visibleLines.contains(*itr))
(*itr)->visibilityChanged(true);
visibleLines.removeOne(*itr);
}
for(ChatLine* line : visibleLines)
line->visibilityChanged(false);
visibleLines = newVisibleLines;
// assure order
std::sort(visibleLines.begin(), visibleLines.end(), [](const ChatLine* lhs, const ChatLine* rhs)
{
return lhs->getRowIndex() < rhs->getRowIndex();
});
//if(!visibleLines.empty())
// qDebug() << "visible from " << visibleLines.first()->getRowIndex() << "to " << visibleLines.last()->getRowIndex() << " total " << visibleLines.size();
}
void ChatLog::scrollContentsBy(int dx, int dy)
{
QGraphicsView::scrollContentsBy(dx, dy);
partialUpdate();
}
void ChatLog::resizeEvent(QResizeEvent* ev)
{
bool stb = stickToBottom();
QGraphicsView::resizeEvent(ev);
if(lines.count() > 300)
partialUpdate();
else
fullUpdate();
if(stb)
scrollToBottom();
}