/* * 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(Shared::Global::getInstance()->titleFont), dividerMetrics(Shared::Global::getInstance()->titleFontMetrics), 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); } 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(); } 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); } } QString FeedView::getSelectedText() const{return selectedText;} //TODO!!! void FeedView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint) {} 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"; const QAbstractItemModel* m = model(); if (m == nullptr) return QAbstractItemView::updateGeometries(); QScrollBar* bar = verticalScrollBar(); const QStyle* st = style(); QSize layoutBounds = maximumViewportSize(); QStyleOptionViewItem option; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) initViewItemOption(&option); #else option = viewOptions(); #endif 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 = itemDelegateForIndex(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 = itemDelegateForIndex(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; } } QStyleOptionViewItem option; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) initViewItemOption(&option); #else option = viewOptions(); #endif QPainter painter(vp); 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); itemDelegateForIndex(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->position().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->position().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::mouseDoubleClickEvent(QMouseEvent* event) { QAbstractItemView::mouseDoubleClickEvent(event); mousePressed = event->button() == Qt::LeftButton; if (mousePressed) { dragStartPoint = event->position().toPoint(); if (specialDelegate && specialModel) { MessageDelegate* del = static_cast(itemDelegate()); QString lastSelectedId = del->clearSelection(); selectedText = ""; if (lastSelectedId.size()) { Models::MessageFeed* feed = static_cast(model()); QModelIndex index = feed->modelIndexById(lastSelectedId); if (index.isValid()) setDirtyRegion(visualRect(index)); } QModelIndex index = indexAt(dragStartPoint); QRect rect = visualRect(index); if (rect.contains(dragStartPoint)) { selectedText = del->leftDoubleClick(dragStartPoint, index, rect); if (selectedText.size() > 0) setDirtyRegion(rect); } } } } void FeedView::mouseReleaseEvent(QMouseEvent* event) { QAbstractItemView::mouseReleaseEvent(event); if (mousePressed) { if (!dragging && specialDelegate) { QPoint point = event->position().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); } 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(); } }