/* * 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 #include #include #include #include "messagedelegate.h" #include "messagefeed.h" constexpr int avatarHeight = 50; constexpr int margin = 6; constexpr int textMargin = 2; constexpr int statusIconSize = 16; MessageDelegate::MessageDelegate(QObject* parent): QStyledItemDelegate(parent), bodyFont(), nickFont(), dateFont(), bodyMetrics(bodyFont), nickMetrics(nickFont), dateMetrics(dateFont), buttonHeight(0), barHeight(0), buttons(new std::map()), bars(new std::map()), statusIcons(new std::map()), pencilIcons(new std::map()), bodies(new std::map()), previews(new std::map()), idsToKeep(new std::set()), clearingWidgets(false) { QPushButton btn; buttonHeight = btn.sizeHint().height(); QProgressBar bar; barHeight = bar.sizeHint().height(); } MessageDelegate::~MessageDelegate() { for (const std::pair& pair: *buttons){ delete pair.second; } for (const std::pair& pair: *bars){ delete pair.second; } for (const std::pair& pair: *statusIcons){ delete pair.second; } for (const std::pair& pair: *pencilIcons){ delete pair.second; } for (const std::pair& pair: *bodies){ delete pair.second; } for (const std::pair& pair: *previews){ delete pair.second; } delete statusIcons; delete pencilIcons; delete idsToKeep; delete buttons; delete bars; delete bodies; delete previews; } void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QVariant vi = index.data(Models::MessageFeed::Bulk); if (!vi.isValid()) { return; } Models::FeedItem data = qvariant_cast(vi); painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); if (option.state & QStyle::State_MouseOver) { painter->fillRect(option.rect, option.palette.brush(QPalette::Inactive, QPalette::Highlight)); } QIcon icon(data.avatar); if (data.sentByMe) { painter->drawPixmap(option.rect.width() - avatarHeight - margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight)); } else { painter->drawPixmap(margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight)); } QStyleOptionViewItem opt = option; QRect messageRect = option.rect.adjusted(margin, margin / 2, -(avatarHeight + 2 * margin), -margin / 2); if (!data.sentByMe) { opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop; messageRect.adjust(avatarHeight + margin, 0, avatarHeight + margin, 0); } else { opt.displayAlignment = Qt::AlignRight | Qt::AlignTop; } opt.rect = messageRect; QSize messageSize(0, 0); QSize bodySize(0, 0); if (data.text.size() > 0) { messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size(); bodySize = messageSize; } messageSize.rheight() += nickMetrics.lineSpacing(); messageSize.rheight() += dateMetrics.height(); if (messageSize.width() < opt.rect.width()) { QSize senderSize = nickMetrics.boundingRect(messageRect, 0, data.sender).size(); if (senderSize.width() > messageSize.width()) { messageSize.setWidth(senderSize.width()); } QSize dateSize = dateMetrics.boundingRect(messageRect, 0, data.date.toLocalTime().toString()).size(); int addition = 0; if (data.correction.corrected) { addition += margin + statusIconSize; } if (data.sentByMe) { addition += margin + statusIconSize; } if (dateSize.width() + addition > messageSize.width()) { messageSize.setWidth(dateSize.width() + addition); } } else { messageSize.setWidth(opt.rect.width()); } QRect rect; painter->setFont(nickFont); painter->drawText(opt.rect, opt.displayAlignment, data.sender, &rect); opt.rect.adjust(0, rect.height() + textMargin, 0, 0); painter->save(); switch (data.attach.state) { case Models::none: clearHelperWidget(data); //i can't imagine the situation where it's gonna be needed break; //but it's a possible performance problem case Models::uploading: paintPreview(data, painter, opt); case Models::downloading: paintBar(getBar(data), painter, data.sentByMe, opt); break; case Models::remote: paintButton(getButton(data), painter, data.sentByMe, opt); break; case Models::ready: case Models::local: clearHelperWidget(data); paintPreview(data, painter, opt); break; case Models::errorDownload: { paintButton(getButton(data), painter, data.sentByMe, opt); paintComment(data, painter, opt); } break; case Models::errorUpload:{ clearHelperWidget(data); paintPreview(data, painter, opt); paintComment(data, painter, opt); } break; } painter->restore(); int messageLeft = INT16_MAX; int messageRight = opt.rect.x() + messageSize.width(); QWidget* vp = static_cast(painter->device()); if (data.text.size() > 0) { QLabel* body = getBody(data); body->setParent(vp); body->setMaximumWidth(bodySize.width()); body->setMinimumWidth(bodySize.width()); body->setMinimumHeight(bodySize.height()); body->setMaximumHeight(bodySize.height()); body->setAlignment(opt.displayAlignment); messageLeft = opt.rect.x(); if (data.sentByMe) { messageLeft = opt.rect.topRight().x() - bodySize.width(); } body->move(messageLeft, opt.rect.y()); body->show(); opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0); } painter->setFont(dateFont); QColor q = painter->pen().color(); q.setAlpha(180); painter->setPen(q); painter->drawText(opt.rect, opt.displayAlignment, data.date.toLocalTime().toString(), &rect); int currentY = opt.rect.y(); if (data.sentByMe) { QLabel* statusIcon = getStatusIcon(data); statusIcon->setParent(vp); statusIcon->move(opt.rect.topRight().x() - messageSize.width(), currentY); statusIcon->show(); opt.rect.adjust(0, statusIconSize + textMargin, 0, 0); } if (data.correction.corrected) { QLabel* pencilIcon = getPencilIcon(data); pencilIcon->setParent(vp); if (data.sentByMe) { pencilIcon->move(opt.rect.topRight().x() - messageSize.width() + statusIconSize + margin, currentY); } else { pencilIcon->move(messageRight - statusIconSize - margin, currentY); } pencilIcon->show(); } else { std::map::const_iterator itr = pencilIcons->find(data.id); if (itr != pencilIcons->end()) { delete itr->second; pencilIcons->erase(itr); } } painter->restore(); if (clearingWidgets) { idsToKeep->insert(data.id); } } QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QRect messageRect = option.rect.adjusted(0, margin / 2, -(avatarHeight + 3 * margin), -margin / 2); QStyleOptionViewItem opt = option; opt.rect = messageRect; QVariant va = index.data(Models::MessageFeed::Attach); Models::Attachment attach = qvariant_cast(va); QString body = index.data(Models::MessageFeed::Text).toString(); QSize messageSize(0, 0); if (body.size() > 0) { messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, body).size(); messageSize.rheight() += textMargin; } switch (attach.state) { case Models::none: break; case Models::uploading: messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; case Models::downloading: messageSize.rheight() += barHeight + textMargin; break; case Models::remote: messageSize.rheight() += buttonHeight + textMargin; break; case Models::ready: case Models::local: messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; break; case Models::errorDownload: messageSize.rheight() += buttonHeight + textMargin; messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin; break; case Models::errorUpload: messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin; break; } messageSize.rheight() += nickMetrics.lineSpacing(); messageSize.rheight() += textMargin; messageSize.rheight() += dateMetrics.height() > statusIconSize ? dateMetrics.height() : statusIconSize; if (messageSize.height() < avatarHeight) { messageSize.setHeight(avatarHeight); } messageSize.rheight() += margin; return messageSize; } void MessageDelegate::initializeFonts(const QFont& font) { bodyFont = font; nickFont = font; dateFont = font; nickFont.setBold(true); float ndps = nickFont.pointSizeF(); if (ndps != -1) { nickFont.setPointSizeF(ndps * 1.2); } else { nickFont.setPointSize(nickFont.pointSize() + 2); } dateFont.setItalic(true); float dps = dateFont.pointSizeF(); if (dps != -1) { dateFont.setPointSizeF(dps * 0.8); } else { dateFont.setPointSize(dateFont.pointSize() - 2); } bodyMetrics = QFontMetrics(bodyFont); nickMetrics = QFontMetrics(nickFont); dateMetrics = QFontMetrics(dateFont); Preview::initializeFont(bodyFont); } bool MessageDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) { //qDebug() << event->type(); return QStyledItemDelegate::editorEvent(event, model, option, index); } void MessageDelegate::paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const { QPoint start; if (sentByMe) { start = {option.rect.width() - btn->width(), option.rect.top()}; } else { start = option.rect.topLeft(); } QWidget* vp = static_cast(painter->device()); btn->setParent(vp); btn->move(start); btn->show(); option.rect.adjust(0, buttonHeight + textMargin, 0, 0); } void MessageDelegate::paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const { painter->setFont(dateFont); QColor q = painter->pen().color(); q.setAlpha(180); painter->setPen(q); QRect rect; painter->drawText(option.rect, option.displayAlignment, data.attach.error, &rect); option.rect.adjust(0, rect.height() + textMargin, 0, 0); } void MessageDelegate::paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const { QPoint start = option.rect.topLeft(); bar->resize(option.rect.width(), barHeight); painter->translate(start); bar->render(painter, QPoint(), QRegion(), QWidget::DrawChildren); option.rect.adjust(0, barHeight + textMargin, 0, 0); } void MessageDelegate::paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const { Preview* preview = 0; std::map::iterator itr = previews->find(data.id); QSize size = option.rect.size(); if (itr != previews->end()) { preview = itr->second; preview->actualize(data.attach.localPath, size, option.rect.topLeft()); } else { QWidget* vp = static_cast(painter->device()); preview = new Preview(data.attach.localPath, size, option.rect.topLeft(), data.sentByMe, vp); previews->insert(std::make_pair(data.id, preview)); } if (!preview->isFileReachable()) { //this is the situation when the file preview couldn't be painted because the file was moved emit invalidPath(data.id); //or deleted. This signal notifies the model, and the model notifies the core, preview can } //handle being invalid for as long as I need and can be even become valid again with a new path option.rect.adjust(0, preview->size().height() + textMargin, 0, 0); } QPushButton * MessageDelegate::getButton(const Models::FeedItem& data) const { std::map::const_iterator itr = buttons->find(data.id); FeedButton* result = 0; if (itr != buttons->end()) { result = itr->second; } else { std::map::const_iterator barItr = bars->find(data.id); if (barItr != bars->end()) { delete barItr->second; bars->erase(barItr); } } if (result == 0) { result = new FeedButton(); result->messageId = data.id; result->setText(QCoreApplication::translate("MessageLine", "Download")); buttons->insert(std::make_pair(data.id, result)); connect(result, &QPushButton::clicked, this, &MessageDelegate::onButtonPushed); } return result; } QProgressBar * MessageDelegate::getBar(const Models::FeedItem& data) const { std::map::const_iterator barItr = bars->find(data.id); QProgressBar* result = 0; if (barItr != bars->end()) { result = barItr->second; } else { std::map::const_iterator itr = buttons->find(data.id); if (itr != buttons->end()) { delete itr->second; buttons->erase(itr); } } if (result == 0) { result = new QProgressBar(); result->setRange(0, 100); bars->insert(std::make_pair(data.id, result)); } result->setValue(data.attach.progress * 100); return result; } QLabel * MessageDelegate::getStatusIcon(const Models::FeedItem& data) const { std::map::const_iterator itr = statusIcons->find(data.id); QLabel* result = 0; if (itr != statusIcons->end()) { result = itr->second; } else { result = new QLabel(); statusIcons->insert(std::make_pair(data.id, result)); } QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast(data.state)])); QString tt = Shared::Global::getName(data.state); if (data.state == Shared::Message::State::error) { if (data.error > 0) { tt += ": " + data.error; } } if (result->toolTip() != tt) { //If i just assign pixmap every time unconditionally result->setPixmap(q.pixmap(statusIconSize)); //it invokes an infinite cycle of repaint result->setToolTip(tt); //may be it's better to subclass and store last condition in int? } return result; } QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const { std::map::const_iterator itr = pencilIcons->find(data.id); QLabel* result = 0; if (itr != pencilIcons->end()) { result = itr->second; } else { result = new QLabel(); QIcon icon = Shared::icon("edit-rename"); result->setPixmap(icon.pixmap(statusIconSize)); pencilIcons->insert(std::make_pair(data.id, result)); } result->setToolTip("Last time edited: " + data.correction.lastCorrection.toLocalTime().toString() + "\nOriginal message: " + data.correction.original); return result; } QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const { std::map::const_iterator itr = bodies->find(data.id); QLabel* result = 0; if (itr != bodies->end()) { result = itr->second; } else { result = new QLabel(); result->setFont(bodyFont); result->setWordWrap(true); result->setOpenExternalLinks(true); result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); bodies->insert(std::make_pair(data.id, result)); } result->setText(Shared::processMessageBody(data.text)); return result; } void MessageDelegate::beginClearWidgets() { idsToKeep->clear(); clearingWidgets = true; } template void removeElements(std::map* elements, std::set* idsToKeep) { std::set toRemove; for (const std::pair& pair: *elements) { if (idsToKeep->find(pair.first) == idsToKeep->end()) { delete pair.second; toRemove.insert(pair.first); } } for (const QString& key : toRemove) { elements->erase(key); } } void MessageDelegate::endClearWidgets() { if (clearingWidgets) { removeElements(buttons, idsToKeep); removeElements(bars, idsToKeep); removeElements(statusIcons, idsToKeep); removeElements(pencilIcons, idsToKeep); removeElements(bodies, idsToKeep); removeElements(previews, idsToKeep); idsToKeep->clear(); clearingWidgets = false; } } void MessageDelegate::onButtonPushed() const { FeedButton* btn = static_cast(sender()); emit buttonPushed(btn->messageId); } void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const { std::map::const_iterator itr = buttons->find(data.id); if (itr != buttons->end()) { delete itr->second; buttons->erase(itr); } else { std::map::const_iterator barItr = bars->find(data.id); if (barItr != bars->end()) { delete barItr->second; bars->erase(barItr); } } } // void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const // { // // }