/* * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify * 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. * * This program is distributed in the hope that it will be useful, * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "feedview.h" #include #include #include #include #include #include #include "messagedelegate.h" #include "messagefeed.h" constexpr int maxMessageHeight = 10000; constexpr int approximateSingleMessageHeight = 20; constexpr int progressSize = 70; constexpr int dateDeviderMargin = 10; const std::set FeedView::geometryChangingRoles = { Models::MessageFeed::Attach, Models::MessageFeed::Text, Models::MessageFeed::Id, Models::MessageFeed::Error, Models::MessageFeed::Date }; FeedView::FeedView(QWidget* parent): QAbstractItemView(parent), hints(), vo(0), elementMargin(0), specialDelegate(false), specialModel(false), clearWidgetsMode(false), modelState(Models::MessageFeed::complete), progress(), dividerFont(), dividerMetrics(dividerFont), mousePressed(false), dragging(false), hovered(Shared::Hover::nothing), dragStartPoint(), dragEndPoint(), selectedText() { horizontalScrollBar()->setRange(0, 0); verticalScrollBar()->setSingleStep(approximateSingleMessageHeight); setMouseTracking(true); setSelectionBehavior(SelectItems); // viewport()->setAttribute(Qt::WA_Hover, true); progress.setParent(viewport()); progress.resize(progressSize, progressSize); dividerFont = getFont(); dividerFont.setBold(true); float ndps = dividerFont.pointSizeF(); if (ndps != -1) { dividerFont.setPointSizeF(ndps * 1.2); } else { dividerFont.setPointSize(dividerFont.pointSize() + 2); } } FeedView::~FeedView() { } QModelIndex FeedView::indexAt(const QPoint& point) const { int32_t vh = viewport()->height(); uint32_t y = vh - point.y() + vo; for (std::deque::size_type i = 0; i < hints.size(); ++i) { const Hint& hint = hints[i]; if (y <= hint.offset + hint.height) { if (y > hint.offset) { return model()->index(i, 0, rootIndex()); } else { break; } } } return QModelIndex(); } void FeedView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint) { } QRect FeedView::visualRect(const QModelIndex& index) const { unsigned int row = index.row(); if (!index.isValid() || row >= hints.size()) { qDebug() << "visualRect for" << row; return QRect(); } else { const Hint& hint = hints.at(row); const QWidget* vp = viewport(); return QRect(hint.x, vp->height() - hint.height - hint.offset + vo, hint.width, hint.height); } } int FeedView::horizontalOffset() const { return 0; } bool FeedView::isIndexHidden(const QModelIndex& index) const { return false; } QModelIndex FeedView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { return QModelIndex(); } void FeedView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command) { } int FeedView::verticalOffset() const { return vo; } QRegion FeedView::visualRegionForSelection(const QItemSelection& selection) const { return QRegion(); } void FeedView::rowsInserted(const QModelIndex& parent, int start, int end) { QAbstractItemView::rowsInserted(parent, start, end); scheduleDelayedItemsLayout(); } void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) { if (specialDelegate) { for (int role : roles) { if (geometryChangingRoles.count(role) != 0) { scheduleDelayedItemsLayout(); //to recalculate layout only if there are some geometry changing modifications break; } } } QAbstractItemView::dataChanged(topLeft, bottomRight, roles); } void FeedView::updateGeometries() { //qDebug() << "updateGeometries"; QScrollBar* bar = verticalScrollBar(); const QStyle* st = style(); const QAbstractItemModel* m = model(); QSize layoutBounds = maximumViewportSize(); QStyleOptionViewItem option = viewOptions(); option.rect.setHeight(maxMessageHeight); option.rect.setWidth(layoutBounds.width()); int frameAroundContents = 0; int verticalScrollBarExtent = st->pixelMetric(QStyle::PM_ScrollBarExtent, 0, bar); bool layedOut = false; if (verticalScrollBarExtent != 0 && verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded && m->rowCount() * approximateSingleMessageHeight < layoutBounds.height()) { hints.clear(); layedOut = tryToCalculateGeometriesWithNoScrollbars(option, m, layoutBounds.height()); } if (layedOut) { bar->setRange(0, 0); vo = 0; } else { int verticalMargin = 0; if (st->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) { frameAroundContents = st->pixelMetric(QStyle::PM_DefaultFrameWidth) * 2; } if (verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded) { verticalMargin = verticalScrollBarExtent + frameAroundContents; } layoutBounds.rwidth() -= verticalMargin; option.features |= QStyleOptionViewItem::WrapText; option.rect.setWidth(layoutBounds.width()); hints.clear(); uint32_t previousOffset = elementMargin; QDateTime lastDate; for (int i = 0, size = m->rowCount(); i < size; ++i) { QModelIndex index = m->index(i, 0, rootIndex()); QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); if (i > 0) { if (currentDate.daysTo(lastDate) > 0) { previousOffset += dividerMetrics.height() + dateDeviderMargin * 2; } else { previousOffset += elementMargin; } } lastDate = currentDate; QSize messageSize = itemDelegate(index)->sizeHint(option, index); uint32_t offsetX(0); if (specialDelegate) { if (index.data(Models::MessageFeed::SentByMe).toBool()) { offsetX = layoutBounds.width() - messageSize.width() - MessageDelegate::avatarHeight - MessageDelegate::margin * 2; } else { offsetX = MessageDelegate::avatarHeight + MessageDelegate::margin * 2; } } hints.emplace_back(Hint({ false, previousOffset, static_cast(messageSize.height()), static_cast(messageSize.width()), offsetX })); previousOffset += messageSize.height(); } int totalHeight = previousOffset - layoutBounds.height() + dividerMetrics.height() + dateDeviderMargin * 2; if (modelState != Models::MessageFeed::complete) { totalHeight += progressSize; } vo = qMax(qMin(vo, totalHeight), 0); bar->setRange(0, totalHeight); bar->setPageStep(layoutBounds.height()); bar->setValue(totalHeight - vo); } positionProgress(); if (specialDelegate) { clearWidgetsMode = true; } QAbstractItemView::updateGeometries(); } bool FeedView::tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* m, uint32_t totalHeight) { uint32_t previousOffset = elementMargin; QDateTime lastDate; for (int i = 0, size = m->rowCount(); i < size; ++i) { QModelIndex index = m->index(i, 0, rootIndex()); QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); if (i > 0) { if (currentDate.daysTo(lastDate) > 0) { previousOffset += dateDeviderMargin * 2 + dividerMetrics.height(); } else { previousOffset += elementMargin; } } lastDate = currentDate; QSize messageSize = itemDelegate(index)->sizeHint(option, index); if (previousOffset + messageSize.height() + elementMargin > totalHeight) { return false; } uint32_t offsetX(0); if (specialDelegate) { if (index.data(Models::MessageFeed::SentByMe).toBool()) { offsetX = option.rect.width() - messageSize.width() - MessageDelegate::avatarHeight - MessageDelegate::margin * 2; } else { offsetX = MessageDelegate::avatarHeight + MessageDelegate::margin * 2; } } hints.emplace_back(Hint({ false, previousOffset, static_cast(messageSize.height()), static_cast(messageSize.width()), offsetX })); previousOffset += messageSize.height(); } previousOffset += dateDeviderMargin * 2 + dividerMetrics.height(); if (previousOffset > totalHeight) { return false; } return true; } void FeedView::paintEvent(QPaintEvent* event) { //qDebug() << "paint" << event->rect(); const QAbstractItemModel* m = model(); QWidget* vp = viewport(); QRect zone = event->rect().translated(0, -vo); uint32_t vph = vp->height(); int32_t y1 = zone.y(); int32_t y2 = y1 + zone.height(); bool inZone = false; std::deque toRener; for (std::deque::size_type i = 0; i < hints.size(); ++i) { const Hint& hint = hints[i]; int32_t relativeY1 = vph - hint.offset - hint.height; if (!inZone) { if (y2 > relativeY1) { inZone = true; } } if (inZone) { toRener.emplace_back(m->index(i, 0, rootIndex())); } if (y1 > relativeY1) { inZone = false; break; } } QPainter painter(vp); QStyleOptionViewItem option = viewOptions(); option.features = QStyleOptionViewItem::WrapText; QPoint cursor = vp->mapFromGlobal(QCursor::pos()); if (specialDelegate) { MessageDelegate* del = static_cast(itemDelegate()); if (clearWidgetsMode) { del->beginClearWidgets(); } } QDateTime lastDate; bool first = true; QRect viewportRect = vp->rect(); for (const QModelIndex& index : toRener) { QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); option.rect = visualRect(index); if (first) { int ind = index.row() - 1; if (ind > 0) { QDateTime underDate = m->index(ind, 0, rootIndex()).data(Models::MessageFeed::Date).toDateTime(); if (currentDate.daysTo(underDate) > 0) { drawDateDevider(option.rect.bottom(), underDate, painter); } } first = false; } QRect stripe = option.rect; stripe.setLeft(0); stripe.setWidth(viewportRect.width()); bool mouseOver = stripe.contains(cursor) && viewportRect.contains(cursor); option.state.setFlag(QStyle::State_MouseOver, mouseOver); itemDelegate(index)->paint(&painter, option, index); if (!lastDate.isNull() && currentDate.daysTo(lastDate) > 0) { drawDateDevider(option.rect.bottom(), lastDate, painter); } lastDate = currentDate; } if (!lastDate.isNull() && inZone) { //if after drawing all messages there is still space drawDateDevider(option.rect.top() - dateDeviderMargin * 2 - dividerMetrics.height(), lastDate, painter); } if (clearWidgetsMode && specialDelegate) { MessageDelegate* del = static_cast(itemDelegate()); del->endClearWidgets(); clearWidgetsMode = false; } } void FeedView::drawDateDevider(int top, const QDateTime& date, QPainter& painter) { int divisionHeight = dateDeviderMargin * 2 + dividerMetrics.height(); QRect r(QPoint(0, top), QSize(viewport()->width(), divisionHeight)); painter.save(); painter.setFont(dividerFont); painter.drawText(r, Qt::AlignCenter, date.toString("d MMMM")); painter.restore(); } void FeedView::verticalScrollbarValueChanged(int value) { vo = verticalScrollBar()->maximum() - value; positionProgress(); if (specialDelegate) { clearWidgetsMode = true; } if (modelState == Models::MessageFeed::incomplete && value < progressSize) { model()->fetchMore(rootIndex()); } QAbstractItemView::verticalScrollbarValueChanged(vo); } void FeedView::setAnchorHovered(Shared::Hover type) { if (hovered != type) { hovered = type; switch (hovered) { case Shared::Hover::nothing: setCursor(Qt::ArrowCursor); break; case Shared::Hover::text: setCursor(Qt::IBeamCursor); break; case Shared::Hover::anchor: setCursor(Qt::PointingHandCursor); break; } } } void FeedView::mouseMoveEvent(QMouseEvent* event) { if (!isVisible()) { return; } dragEndPoint = event->localPos().toPoint(); if (mousePressed) { QPoint distance = dragStartPoint - dragEndPoint; if (distance.manhattanLength() > 5) { dragging = true; } } QAbstractItemView::mouseMoveEvent(event); if (specialDelegate) { MessageDelegate* del = static_cast(itemDelegate()); if (dragging) { QModelIndex index = indexAt(dragStartPoint); if (index.isValid()) { QRect rect = visualRect(index); if (rect.contains(dragStartPoint)) { QString selected = del->mouseDrag(dragStartPoint, dragEndPoint, index, rect); if (selectedText != selected) { selectedText = selected; setDirtyRegion(rect); } } } } else { QModelIndex index = indexAt(dragEndPoint); if (index.isValid()) { QRect rect = visualRect(index); if (rect.contains(dragEndPoint)) { setAnchorHovered(del->hoverType(dragEndPoint, index, rect)); } else { setAnchorHovered(Shared::Hover::nothing); } } else { setAnchorHovered(Shared::Hover::nothing); } } } } void FeedView::mousePressEvent(QMouseEvent* event) { QAbstractItemView::mousePressEvent(event); mousePressed = event->button() == Qt::LeftButton; if (mousePressed) { dragStartPoint = event->localPos().toPoint(); if (specialDelegate && specialModel) { MessageDelegate* del = static_cast(itemDelegate()); QString lastSelectedId = del->clearSelection(); if (lastSelectedId.size()) { Models::MessageFeed* feed = static_cast(model()); QModelIndex index = feed->modelIndexById(lastSelectedId); if (index.isValid()) { setDirtyRegion(visualRect(index)); } } } } } void FeedView::mouseReleaseEvent(QMouseEvent* event) { QAbstractItemView::mouseReleaseEvent(event); if (mousePressed) { if (!dragging && specialDelegate) { QPoint point = event->localPos().toPoint(); QModelIndex index = indexAt(point); if (index.isValid()) { QRect rect = visualRect(index); MessageDelegate* del = static_cast(itemDelegate()); if (rect.contains(point)) { del->leftClick(point, index, rect); } } } dragging = false; mousePressed = false; } } void FeedView::keyPressEvent(QKeyEvent* event) { QKeyEvent *key_event = static_cast(event); if (key_event->matches(QKeySequence::Copy)) { if (selectedText.size() > 0) { QClipboard* cb = QApplication::clipboard(); cb->setText(selectedText); } } } void FeedView::resizeEvent(QResizeEvent* event) { QAbstractItemView::resizeEvent(event); positionProgress(); emit resized(); } void FeedView::positionProgress() { QSize layoutBounds = maximumViewportSize(); int progressPosition = layoutBounds.height() - progressSize; std::deque::size_type size = hints.size(); if (size > 0) { const Hint& hint = hints[size - 1]; progressPosition -= hint.offset + hint.height; } progressPosition += vo; progressPosition = qMin(progressPosition, 0); progress.move((width() - progressSize) / 2, progressPosition); } QFont FeedView::getFont() const { return viewOptions().font; } void FeedView::setItemDelegate(QAbstractItemDelegate* delegate) { if (specialDelegate) { MessageDelegate* del = static_cast(itemDelegate()); disconnect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); disconnect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); } QAbstractItemView::setItemDelegate(delegate); MessageDelegate* del = dynamic_cast(delegate); if (del) { specialDelegate = true; elementMargin = MessageDelegate::margin; connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); connect(del, &MessageDelegate::openLink, &QDesktopServices::openUrl); } else { specialDelegate = false; elementMargin = 0; } } void FeedView::setModel(QAbstractItemModel* p_model) { if (specialModel) { Models::MessageFeed* feed = static_cast(model()); disconnect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange); } QAbstractItemView::setModel(p_model); Models::MessageFeed* feed = dynamic_cast(p_model); if (feed) { onModelSyncStateChange(feed->getSyncState()); specialModel = true; connect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange); } else { onModelSyncStateChange(Models::MessageFeed::complete); specialModel = false; } } void FeedView::onMessageButtonPushed(const QString& messageId) { if (specialModel) { Models::MessageFeed* feed = static_cast(model()); feed->downloadAttachment(messageId); } } void FeedView::onMessageInvalidPath(const QString& messageId) { if (specialModel) { Models::MessageFeed* feed = static_cast(model()); feed->reportLocalPathInvalid(messageId); } } void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state) { bool needToUpdateGeometry = false; if (modelState != state) { if (state == Models::MessageFeed::complete || modelState == Models::MessageFeed::complete) { needToUpdateGeometry = true; } modelState = state; if (state == Models::MessageFeed::syncing) { progress.show(); progress.start(); } else { progress.stop(); progress.hide(); } } if (needToUpdateGeometry) { scheduleDelayedItemsLayout(); } } QString FeedView::getSelectedText() const { return selectedText; }