message preview refactor, several bugs about label size, animations are now playing in previews

This commit is contained in:
Blue 2021-05-16 01:07:49 +03:00
parent 4307262f6e
commit 0d584c5aba
Signed by untrusted user: blue
GPG key ID: 9B203B252A63EE38
20 changed files with 498 additions and 164 deletions

View file

@ -5,18 +5,10 @@ target_sources(squawk PRIVATE
comboboxdelegate.h
exponentialblur.cpp
exponentialblur.h
feedview.cpp
feedview.h
flowlayout.cpp
flowlayout.h
image.cpp
image.h
message.cpp
message.h
messagedelegate.cpp
messagedelegate.h
messageline.cpp
messageline.h
progress.cpp
progress.h
resizer.cpp

View file

@ -1,434 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "feedview.h"
#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QDebug>
#include "messagedelegate.h"
#include "ui/models/messagefeed.h"
constexpr int maxMessageHeight = 10000;
constexpr int approximateSingleMessageHeight = 20;
constexpr int progressSize = 70;
const std::set<int> 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),
specialDelegate(false),
specialModel(false),
clearWidgetsMode(false),
modelState(Models::MessageFeed::complete),
progress()
{
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<Hint>::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(0, vp->height() - hint.height - hint.offset + vo, vp->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<int>& 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 = 0;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
int totalHeight = previousOffset - layoutBounds.height();
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 = 0;
bool success = true;
for (int i = 0, size = m->rowCount(); i < size; ++i) {
QModelIndex index = m->index(i, 0, rootIndex());
int height = itemDelegate(index)->sizeHint(option, index).height();
if (previousOffset + height > totalHeight) {
success = false;
break;
}
hints.emplace_back(Hint({
false,
previousOffset,
static_cast<uint32_t>(height)
}));
previousOffset += height;
}
return success;
}
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<QModelIndex> toRener;
for (std::deque<Hint>::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) {
break;
}
}
QPainter painter(vp);
QStyleOptionViewItem option = viewOptions();
option.features = QStyleOptionViewItem::WrapText;
QPoint cursor = vp->mapFromGlobal(QCursor::pos());
if (specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
if (clearWidgetsMode) {
del->beginClearWidgets();
}
}
for (const QModelIndex& index : toRener) {
option.rect = visualRect(index);
bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor);
option.state.setFlag(QStyle::State_MouseOver, mouseOver);
itemDelegate(index)->paint(&painter, option, index);
}
if (clearWidgetsMode && specialDelegate) {
MessageDelegate* del = static_cast<MessageDelegate*>(itemDelegate());
del->endClearWidgets();
clearWidgetsMode = false;
}
if (event->rect().height() == vp->height()) {
// draw the blurred drop shadow...
}
}
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::mouseMoveEvent(QMouseEvent* event)
{
if (!isVisible()) {
return;
}
QAbstractItemView::mouseMoveEvent(event);
}
void FeedView::resizeEvent(QResizeEvent* event)
{
QAbstractItemView::resizeEvent(event);
positionProgress();
emit resized();
}
void FeedView::positionProgress()
{
QSize layoutBounds = maximumViewportSize();
int progressPosition = layoutBounds.height() - progressSize;
std::deque<Hint>::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<MessageDelegate*>(itemDelegate());
disconnect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
disconnect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
}
QAbstractItemView::setItemDelegate(delegate);
MessageDelegate* del = dynamic_cast<MessageDelegate*>(delegate);
if (del) {
specialDelegate = true;
connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed);
connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath);
} else {
specialDelegate = false;
}
}
void FeedView::setModel(QAbstractItemModel* p_model)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(model());
disconnect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange);
}
QAbstractItemView::setModel(p_model);
Models::MessageFeed* feed = dynamic_cast<Models::MessageFeed*>(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<Models::MessageFeed*>(model());
feed->downloadAttachment(messageId);
}
}
void FeedView::onMessageInvalidPath(const QString& messageId)
{
if (specialModel) {
Models::MessageFeed* feed = static_cast<Models::MessageFeed*>(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();
}
}

View file

@ -1,95 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FEEDVIEW_H
#define FEEDVIEW_H
#include <QAbstractItemView>
#include <deque>
#include <set>
#include <ui/models/messagefeed.h>
#include "progress.h"
/**
* @todo write docs
*/
class FeedView : public QAbstractItemView
{
Q_OBJECT
public:
FeedView(QWidget* parent = nullptr);
~FeedView();
QModelIndex indexAt(const QPoint & point) const override;
void scrollTo(const QModelIndex & index, QAbstractItemView::ScrollHint hint) override;
QRect visualRect(const QModelIndex & index) const override;
bool isIndexHidden(const QModelIndex & index) const override;
QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
void setSelection(const QRect & rect, QItemSelectionModel::SelectionFlags command) override;
QRegion visualRegionForSelection(const QItemSelection & selection) const override;
void setItemDelegate(QAbstractItemDelegate* delegate);
void setModel(QAbstractItemModel * model) override;
QFont getFont() const;
signals:
void resized();
public slots:
protected slots:
void rowsInserted(const QModelIndex & parent, int start, int end) override;
void verticalScrollbarValueChanged(int value) override;
void dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector<int> & roles) override;
void onMessageButtonPushed(const QString& messageId);
void onMessageInvalidPath(const QString& messageId);
void onModelSyncStateChange(Models::MessageFeed::SyncState state);
protected:
int verticalOffset() const override;
int horizontalOffset() const override;
void paintEvent(QPaintEvent * event) override;
void updateGeometries() override;
void mouseMoveEvent(QMouseEvent * event) override;
void resizeEvent(QResizeEvent * event) override;
private:
bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight);
void positionProgress();
private:
struct Hint {
bool dirty;
uint32_t offset;
uint32_t height;
};
std::deque<Hint> hints;
int vo;
bool specialDelegate;
bool specialModel;
bool clearWidgetsMode;
Models::MessageFeed::SyncState modelState;
Progress progress;
static const std::set<int> geometryChangingRoles;
};
#endif //FEEDVIEW_H

View file

@ -1,344 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "message.h"
#include <QDebug>
#include <QMimeDatabase>
#include <QPixmap>
#include <QFileInfo>
#include <QRegularExpression>
Message::Message(const Shared::Message& source, bool p_outgoing, const QString& p_sender, const QString& avatarPath, QWidget* parent):
QWidget(parent),
outgoing(p_outgoing),
msg(source),
body(new QWidget()),
statusBar(new QWidget()),
bodyLayout(new QVBoxLayout(body)),
layout(new QHBoxLayout(this)),
date(new QLabel(msg.getTime().toLocalTime().toString())),
sender(new QLabel(p_sender)),
text(new QLabel()),
shadow(new QGraphicsDropShadowEffect()),
button(0),
file(0),
progress(0),
fileComment(new QLabel()),
statusIcon(0),
editedLabel(0),
avatar(new Image(avatarPath.size() == 0 ? Shared::iconPath("user", true) : avatarPath, 60)),
hasButton(false),
hasProgress(false),
hasFile(false),
commentAdded(false),
hasStatusIcon(false),
hasEditedLabel(false)
{
setContentsMargins(0, 0, 0, 0);
layout->setContentsMargins(10, 5, 10, 5);
body->setBackgroundRole(QPalette::AlternateBase);
body->setAutoFillBackground(true);
QString bd = Shared::processMessageBody(msg.getBody());
text->setTextFormat(Qt::RichText);
text->setText(bd);;
text->setTextInteractionFlags(text->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
text->setWordWrap(true);
text->setOpenExternalLinks(true);
if (bd.size() == 0) {
text->hide();
}
QFont dFont = date->font();
dFont.setItalic(true);
dFont.setPointSize(dFont.pointSize() - 2);
date->setFont(dFont);
QFont f;
f.setBold(true);
sender->setFont(f);
bodyLayout->addWidget(sender);
bodyLayout->addWidget(text);
shadow->setBlurRadius(10);
shadow->setXOffset(1);
shadow->setYOffset(1);
shadow->setColor(Qt::black);
body->setGraphicsEffect(shadow);
avatar->setMaximumHeight(60);
avatar->setMaximumWidth(60);
statusBar->setContentsMargins(0, 0, 0, 0);
QHBoxLayout* statusLay = new QHBoxLayout();
statusLay->setContentsMargins(0, 0, 0, 0);
statusBar->setLayout(statusLay);
if (outgoing) {
sender->setAlignment(Qt::AlignRight);
date->setAlignment(Qt::AlignRight);
statusIcon = new QLabel();
setState();
statusLay->addWidget(statusIcon);
statusLay->addWidget(date);
layout->addStretch();
layout->addWidget(body);
layout->addWidget(avatar);
hasStatusIcon = true;
} else {
layout->addWidget(avatar);
layout->addWidget(body);
layout->addStretch();
statusLay->addWidget(date);
}
if (msg.getEdited()) {
setEdited();
}
bodyLayout->addWidget(statusBar);
layout->setAlignment(avatar, Qt::AlignTop);
}
Message::~Message()
{
if (!commentAdded) {
delete fileComment;
}
//delete body; //not sure if I should delete it here, it's probably already owned by the infrastructure and gonna die with the rest of the widget
//delete avatar;
}
QString Message::getId() const
{
return msg.getId();
}
QString Message::getSenderJid() const
{
return msg.getFromJid();
}
QString Message::getSenderResource() const
{
return msg.getFromResource();
}
QString Message::getFileUrl() const
{
return msg.getOutOfBandUrl();
}
void Message::setSender(const QString& p_sender)
{
sender->setText(p_sender);
}
void Message::addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip)
{
hideFile();
hideProgress();
if (!hasButton) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
button = new QPushButton(icon, buttonText);
button->setToolTip(tooltip);
connect(button, &QPushButton::clicked, this, &Message::buttonClicked);
bodyLayout->insertWidget(2, button);
hasButton = true;
}
}
void Message::setProgress(qreal value)
{
hideFile();
hideButton();
if (!hasProgress) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
progress = new QProgressBar();
progress->setRange(0, 100);
bodyLayout->insertWidget(2, progress);
hasProgress = true;
}
progress->setValue(value * 100);
}
void Message::showFile(const QString& path)
{
hideButton();
hideProgress();
if (!hasFile) {
hideComment();
if (msg.getBody() == msg.getOutOfBandUrl()) {
text->setText("");
text->hide();
}
QMimeDatabase db;
QMimeType type = db.mimeTypeForFile(path);
QStringList parts = type.name().split("/");
QString big = parts.front();
QFileInfo info(path);
if (big == "image") {
file = new Image(path);
} else {
file = new QLabel();
file->setPixmap(QIcon::fromTheme(type.iconName()).pixmap(50));
file->setAlignment(Qt::AlignCenter);
showComment(info.fileName(), true);
}
file->setContextMenuPolicy(Qt::ActionsContextMenu);
QAction* openAction = new QAction(QIcon::fromTheme("document-new-from-template"), tr("Open"), file);
connect(openAction, &QAction::triggered, [path]() { //TODO need to get rid of this shame
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
file->addAction(openAction);
bodyLayout->insertWidget(2, file);
hasFile = true;
}
}
void Message::hideComment()
{
if (commentAdded) {
bodyLayout->removeWidget(fileComment);
fileComment->hide();
fileComment->setWordWrap(false);
commentAdded = false;
}
}
void Message::hideButton()
{
if (hasButton) {
button->deleteLater();
button = 0;
hasButton = false;
}
}
void Message::hideFile()
{
if (hasFile) {
file->deleteLater();
file = 0;
hasFile = false;
}
}
void Message::hideProgress()
{
if (hasProgress) {
progress->deleteLater();
progress = 0;
hasProgress = false;;
}
}
void Message::showComment(const QString& comment, bool wordWrap)
{
if (!commentAdded) {
int index = 2;
if (hasFile) {
index++;
}
if (hasButton) {
index++;
}
if (hasProgress) {
index++;
}
bodyLayout->insertWidget(index, fileComment);
fileComment->show();
commentAdded = true;
}
fileComment->setWordWrap(wordWrap);
fileComment->setText(comment);
}
const Shared::Message & Message::getMessage() const
{
return msg;
}
void Message::setAvatarPath(const QString& p_path)
{
if (p_path.size() == 0) {
avatar->setPath(Shared::iconPath("user", true));
} else {
avatar->setPath(p_path);
}
}
bool Message::change(const QMap<QString, QVariant>& data)
{
bool idChanged = msg.change(data);
QString body = msg.getBody();
QString bd = Shared::processMessageBody(body);
if (body.size() > 0) {
text->setText(bd);
text->show();
} else {
text->setText(body);
text->hide();
}
if (msg.getEdited()) {
setEdited();
}
if (hasStatusIcon) {
setState();
}
return idChanged;
}
void Message::setEdited()
{
if (!hasEditedLabel) {
editedLabel = new QLabel();
hasEditedLabel = true;
QIcon q(Shared::icon("edit-rename"));
editedLabel->setPixmap(q.pixmap(12, 12));
QHBoxLayout* statusLay = static_cast<QHBoxLayout*>(statusBar->layout());
statusLay->insertWidget(1, editedLabel);
}
editedLabel->setToolTip("Last time edited: " + msg.getLastModified().toLocalTime().toString()
+ "\nOriginal message: " + msg.getOriginalBody());
}
void Message::setState()
{
Shared::Message::State state = msg.getState();
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(state)]));
QString tt = Shared::Global::getName(state);
if (state == Shared::Message::State::error) {
QString errText = msg.getErrorText();
if (errText.size() > 0) {
tt += ": " + errText;
}
}
statusIcon->setToolTip(tt);
statusIcon->setPixmap(q.pixmap(12, 12));
}

View file

@ -1,103 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MESSAGE_H
#define MESSAGE_H
#include <QWidget>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QGraphicsDropShadowEffect>
#include <QPushButton>
#include <QProgressBar>
#include <QAction>
#include <QDesktopServices>
#include <QUrl>
#include <QMap>
#include "shared/message.h"
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
#include "resizer.h"
#include "image.h"
/**
* @todo write docs
*/
class Message : public QWidget
{
Q_OBJECT
public:
Message(const Shared::Message& source, bool outgoing, const QString& sender, const QString& avatarPath = "", QWidget* parent = nullptr);
~Message();
void setSender(const QString& sender);
QString getId() const;
QString getSenderJid() const;
QString getSenderResource() const;
QString getFileUrl() const;
const Shared::Message& getMessage() const;
void addButton(const QIcon& icon, const QString& buttonText, const QString& tooltip = "");
void showComment(const QString& comment, bool wordWrap = false);
void hideComment();
void showFile(const QString& path);
void setProgress(qreal value);
void setAvatarPath(const QString& p_path);
bool change(const QMap<QString, QVariant>& data);
bool const outgoing;
signals:
void buttonClicked();
private:
Shared::Message msg;
QWidget* body;
QWidget* statusBar;
QVBoxLayout* bodyLayout;
QHBoxLayout* layout;
QLabel* date;
QLabel* sender;
QLabel* text;
QGraphicsDropShadowEffect* shadow;
QPushButton* button;
QLabel* file;
QProgressBar* progress;
QLabel* fileComment;
QLabel* statusIcon;
QLabel* editedLabel;
Image* avatar;
bool hasButton;
bool hasProgress;
bool hasFile;
bool commentAdded;
bool hasStatusIcon;
bool hasEditedLabel;
private:
void hideButton();
void hideProgress();
void hideFile();
void setState();
void setEdited();
};
#endif // MESSAGE_H

View file

@ -1,584 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include <QPainter>
#include <QApplication>
#include <QMouseEvent>
#include "messagedelegate.h"
#include "ui/models/messagefeed.h"
constexpr int avatarHeight = 50;
constexpr int margin = 6;
constexpr int textMargin = 2;
constexpr int statusIconSize = 16;
constexpr int maxAttachmentHeight = 500;
MessageDelegate::MessageDelegate(QObject* parent):
QStyledItemDelegate(parent),
bodyFont(),
nickFont(),
dateFont(),
bodyMetrics(bodyFont),
nickMetrics(nickFont),
dateMetrics(dateFont),
buttonHeight(0),
barHeight(0),
buttons(new std::map<QString, FeedButton*>()),
bars(new std::map<QString, QProgressBar*>()),
statusIcons(new std::map<QString, QLabel*>()),
bodies(new std::map<QString, QLabel*>()),
idsToKeep(new std::set<QString>()),
clearingWidgets(false)
{
QPushButton btn;
buttonHeight = btn.sizeHint().height();
QProgressBar bar;
barHeight = bar.sizeHint().height();
}
MessageDelegate::~MessageDelegate()
{
for (const std::pair<const QString, FeedButton*>& pair: *buttons){
delete pair.second;
}
for (const std::pair<const QString, QProgressBar*>& pair: *bars){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *statusIcons){
delete pair.second;
}
for (const std::pair<const QString, QLabel*>& pair: *bodies){
delete pair.second;
}
delete idsToKeep;
delete buttons;
delete bars;
delete bodies;
}
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<Models::FeedItem>(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());
}
} 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);
painter->setFont(dateFont);
QColor q = painter->pen().color();
q.setAlpha(180);
painter->setPen(q);
painter->drawText(opt.rect, opt.displayAlignment, data.attach.error, &rect);
opt.rect.adjust(0, rect.height() + textMargin, 0, 0);
}
break;
case Models::errorUpload:{
clearHelperWidget(data);
paintPreview(data, painter, opt);
painter->setFont(dateFont);
QColor q = painter->pen().color();
q.setAlpha(180);
painter->setPen(q);
painter->drawText(opt.rect, opt.displayAlignment, data.attach.error, &rect);
opt.rect.adjust(0, rect.height() + textMargin, 0, 0);
}
break;
}
painter->restore();
int messageLeft = INT16_MAX;
QWidget* vp = static_cast<QWidget*>(painter->device());
if (data.text.size() > 0) {
QLabel* body = getBody(data);
body->setParent(vp);
body->setMaximumWidth(bodySize.width());
body->setMinimumWidth(bodySize.width());
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);
if (data.sentByMe) {
if (messageLeft > rect.x() - statusIconSize - margin) {
messageLeft = rect.x() - statusIconSize - margin;
}
QLabel* statusIcon = getStatusIcon(data);
statusIcon->setParent(vp);
statusIcon->move(messageLeft, opt.rect.y());
statusIcon->show();
opt.rect.adjust(0, statusIconSize + textMargin, 0, 0);
}
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<Models::Attachment>(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() += 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() += 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() += 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);
}
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<QWidget*>(painter->device());
btn->setParent(vp);
btn->move(start);
btn->show();
option.rect.adjust(0, buttonHeight + 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
{
Shared::Global::FileInfo info = Shared::Global::getFileInfo(data.attach.localPath);
QSize size = constrainAttachSize(info.size, option.rect.size());
QPoint start;
if (data.sentByMe) {
start = {option.rect.width() - size.width(), option.rect.top()};
start.rx() += margin;
} else {
start = option.rect.topLeft();
}
QRect rect(start, size);
switch (info.preview) {
case Shared::Global::FileInfo::Preview::picture: {
QImage img(data.attach.localPath);
if (img.isNull()) {
emit invalidPath(data.id);
} else {
painter->drawImage(rect, img);
}
}
break;
default: {
QIcon icon = QIcon::fromTheme(info.mime.iconName());
painter->save();
painter->setFont(bodyFont);
int labelWidth = option.rect.width() - size.width() - margin;
QString elidedName = bodyMetrics.elidedText(info.name, Qt::ElideMiddle, labelWidth);
QSize nameSize = bodyMetrics.boundingRect(QRect(start, QSize(labelWidth, 0)), 0, elidedName).size();
if (data.sentByMe) {
start.rx() -= nameSize.width() + margin;
}
painter->drawPixmap({start, size}, icon.pixmap(info.size));
start.rx() += size.width() + margin;
start.ry() += nameSize.height() + (size.height() - nameSize.height()) / 2;
painter->drawText(start, elidedName);
painter->restore();
}
}
option.rect.adjust(0, size.height() + textMargin, 0, 0);
}
QPushButton * MessageDelegate::getButton(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
FeedButton* result = 0;
if (itr != buttons->end()) {
result = itr->second;
} else {
std::map<QString, QProgressBar*>::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<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
QProgressBar* result = 0;
if (barItr != bars->end()) {
result = barItr->second;
} else {
std::map<QString, FeedButton*>::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<QString, QLabel*>::const_iterator itr = statusIcons->find(data.id);
QLabel* result = 0;
QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast<uint8_t>(data.state)]));
QString tt = Shared::Global::getName(data.state);
if (data.state == Shared::Message::State::error) {
if (data.error > 0) {
tt += ": " + data.error;
}
}
if (itr != statusIcons->end()) {
result = itr->second;
if (result->toolTip() != tt) { //If i just assign pixmap every time unconditionally
result->setPixmap(q.pixmap(statusIconSize)); //it involves into an infinite cycle of repaint
result->setToolTip(tt); //may be it's better to subclass and store last condition in int?
}
} else {
result = new QLabel();
statusIcons->insert(std::make_pair(data.id, result));
result->setPixmap(q.pixmap(statusIconSize));
result->setToolTip(tt);
}
result->setToolTip(tt);
//result->setText(std::to_string((int)data.state).c_str());
return result;
}
QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const
{
std::map<QString, QLabel*>::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;
}
void MessageDelegate::endClearWidgets()
{
if (clearingWidgets) {
std::set<QString> toRemoveButtons;
std::set<QString> toRemoveBars;
std::set<QString> toRemoveIcons;
std::set<QString> toRemoveBodies;
for (const std::pair<const QString, FeedButton*>& pair: *buttons) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveButtons.insert(pair.first);
}
}
for (const std::pair<const QString, QProgressBar*>& pair: *bars) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveBars.insert(pair.first);
}
}
for (const std::pair<const QString, QLabel*>& pair: *statusIcons) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveIcons.insert(pair.first);
}
}
for (const std::pair<const QString, QLabel*>& pair: *bodies) {
if (idsToKeep->find(pair.first) == idsToKeep->end()) {
delete pair.second;
toRemoveBodies.insert(pair.first);
}
}
for (const QString& key : toRemoveButtons) {
buttons->erase(key);
}
for (const QString& key : toRemoveBars) {
bars->erase(key);
}
for (const QString& key : toRemoveIcons) {
statusIcons->erase(key);
}
for (const QString& key : toRemoveBodies) {
bodies->erase(key);
}
idsToKeep->clear();
clearingWidgets = false;
}
}
void MessageDelegate::onButtonPushed() const
{
FeedButton* btn = static_cast<FeedButton*>(sender());
emit buttonPushed(btn->messageId);
}
void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const
{
std::map<QString, FeedButton*>::const_iterator itr = buttons->find(data.id);
if (itr != buttons->end()) {
delete itr->second;
buttons->erase(itr);
} else {
std::map<QString, QProgressBar*>::const_iterator barItr = bars->find(data.id);
if (barItr != bars->end()) {
delete barItr->second;
bars->erase(barItr);
}
}
}
QSize MessageDelegate::calculateAttachSize(const QString& path, const QRect& bounds) const
{
Shared::Global::FileInfo info = Shared::Global::getFileInfo(path);
return constrainAttachSize(info.size, bounds.size());
}
QSize MessageDelegate::constrainAttachSize(QSize src, QSize bounds) const
{
bounds.setHeight(maxAttachmentHeight);
if (src.width() > bounds.width() || src.height() > bounds.height()) {
src.scale(bounds, Qt::KeepAspectRatio);
}
return src;
}
// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
// {
//
// }

View file

@ -1,101 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MESSAGEDELEGATE_H
#define MESSAGEDELEGATE_H
#include <map>
#include <set>
#include <QStyledItemDelegate>
#include <QStyleOptionButton>
#include <QFont>
#include <QFontMetrics>
#include <QPushButton>
#include <QProgressBar>
#include <QLabel>
#include "shared/icons.h"
#include "shared/global.h"
#include "shared/utils.h"
namespace Models {
struct FeedItem;
};
class MessageDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MessageDelegate(QObject *parent = nullptr);
~MessageDelegate();
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
//void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
void initializeFonts(const QFont& font);
bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override;
void endClearWidgets();
void beginClearWidgets();
signals:
void buttonPushed(const QString& messageId) const;
void invalidPath(const QString& messageId) const;
protected:
void paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const;
void paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const;
QPushButton* getButton(const Models::FeedItem& data) const;
QProgressBar* getBar(const Models::FeedItem& data) const;
QLabel* getStatusIcon(const Models::FeedItem& data) const;
QLabel* getBody(const Models::FeedItem& data) const;
void clearHelperWidget(const Models::FeedItem& data) const;
QSize calculateAttachSize(const QString& path, const QRect& bounds) const;
QSize constrainAttachSize(QSize src, QSize bounds) const;
protected slots:
void onButtonPushed() const;
private:
class FeedButton : public QPushButton {
public:
QString messageId;
};
QFont bodyFont;
QFont nickFont;
QFont dateFont;
QFontMetrics bodyMetrics;
QFontMetrics nickMetrics;
QFontMetrics dateMetrics;
int buttonHeight;
int barHeight;
std::map<QString, FeedButton*>* buttons;
std::map<QString, QProgressBar*>* bars;
std::map<QString, QLabel*>* statusIcons;
std::map<QString, QLabel*>* bodies;
std::set<QString>* idsToKeep;
bool clearingWidgets;
};
#endif // MESSAGEDELEGATE_H

View file

@ -1,504 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "messageline.h"
#include <QDebug>
#include <QCoreApplication>
#include <cmath>
MessageLine::MessageLine(bool p_room, QWidget* parent):
QWidget(parent),
messageIndex(),
messageOrder(),
myMessages(),
palMessages(),
uploadPaths(),
palAvatars(),
exPalAvatars(),
layout(new QVBoxLayout(this)),
myName(),
myAvatarPath(),
palNames(),
uploading(),
downloading(),
room(p_room),
busyShown(false),
progress()
{
setContentsMargins(0, 0, 0, 0);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addStretch();
}
MessageLine::~MessageLine()
{
for (Index::const_iterator itr = messageIndex.begin(), end = messageIndex.end(); itr != end; ++itr) {
delete itr->second;
}
}
MessageLine::Position MessageLine::message(const Shared::Message& msg, bool forceOutgoing)
{
QString id = msg.getId();
Index::iterator itr = messageIndex.find(id);
if (itr != messageIndex.end()) {
qDebug() << "received more then one message with the same id, skipping yet the new one";
return invalid;
}
QString sender;
QString aPath;
bool outgoing;
if (forceOutgoing) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
if (room) {
if (msg.getFromResource() == myName) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
sender = msg.getFromResource();
std::map<QString, QString>::iterator aItr = palAvatars.find(sender);
if (aItr != palAvatars.end()) {
aPath = aItr->second;
} else {
aItr = exPalAvatars.find(sender);
if (aItr != exPalAvatars.end()) {
aPath = aItr->second;
}
}
outgoing = false;
}
} else {
if (msg.getOutgoing()) {
sender = myName;
aPath = myAvatarPath;
outgoing = true;
} else {
QString jid = msg.getFromJid();
std::map<QString, QString>::iterator itr = palNames.find(jid);
if (itr != palNames.end()) {
sender = itr->second;
} else {
sender = jid;
}
std::map<QString, QString>::iterator aItr = palAvatars.find(jid);
if (aItr != palAvatars.end()) {
aPath = aItr->second;
}
outgoing = false;
}
}
}
Message* message = new Message(msg, outgoing, sender, aPath);
std::pair<Order::const_iterator, bool> result = messageOrder.insert(std::make_pair(msg.getTime(), message));
if (!result.second) {
qDebug() << "Error appending a message into a message list - seems like the time of that message exactly matches the time of some other message, can't put them in order, skipping yet";
delete message;
return invalid;
}
if (outgoing) {
myMessages.insert(std::make_pair(id, message));
} else {
QString senderId;
if (room) {
senderId = sender;
} else {
senderId = msg.getFromJid();
}
std::map<QString, Index>::iterator pItr = palMessages.find(senderId);
if (pItr == palMessages.end()) {
pItr = palMessages.insert(std::make_pair(senderId, Index())).first;
}
pItr->second.insert(std::make_pair(id, message));
}
messageIndex.insert(std::make_pair(id, message));
unsigned long index = std::distance<Order::const_iterator>(messageOrder.begin(), result.first); //need to make with binary indexed tree
Position res = invalid;
if (index == 0) {
res = beggining;
} else if (index == messageIndex.size() - 1) {
res = end;
} else {
res = middle;
}
if (busyShown) {
index += 1;
}
if (res == end) {
layout->addWidget(message);
} else {
layout->insertWidget(index + 1, message);
}
if (msg.hasOutOfBandUrl()) {
emit requestLocalFile(msg.getId(), msg.getOutOfBandUrl());
connect(message, &Message::buttonClicked, this, &MessageLine::onDownload);
}
return res;
}
void MessageLine::changeMessage(const QString& id, const QMap<QString, QVariant>& data)
{
Index::const_iterator itr = messageIndex.find(id);
if (itr != messageIndex.end()) {
Message* msg = itr->second;
if (msg->change(data)) { //if ID changed (stanza in replace of another)
QString newId = msg->getId(); //need to updated IDs of that message in all maps
messageIndex.erase(itr);
messageIndex.insert(std::make_pair(newId, msg));
if (msg->outgoing) {
QString senderId;
if (room) {
senderId = msg->getSenderResource();
} else {
senderId = msg->getSenderJid();
}
std::map<QString, Index>::iterator pItr = palMessages.find(senderId);
if (pItr != palMessages.end()) {
Index::const_iterator sItr = pItr->second.find(id);
if (sItr != pItr->second.end()) {
pItr->second.erase(sItr);
pItr->second.insert(std::make_pair(newId, msg));
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find it among pal's messages, probably an error";
}
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find pal messages, probably an error";
}
} else {
Index::const_iterator mItr = myMessages.find(id);
if (mItr != myMessages.end()) {
myMessages.erase(mItr);
myMessages.insert(std::make_pair(newId, msg));
} else {
qDebug() << "Was trying to replace message in open conversations, couldn't find it among my messages, probably an error";
}
}
}
}
}
void MessageLine::onDownload()
{
Message* msg = static_cast<Message*>(sender());
QString messageId = msg->getId();
Index::const_iterator itr = downloading.find(messageId);
if (itr == downloading.end()) {
downloading.insert(std::make_pair(messageId, msg));
msg->setProgress(0);
msg->showComment(tr("Downloading..."));
emit downloadFile(messageId, msg->getFileUrl());
} else {
qDebug() << "An attempt to initiate download for already downloading file" << msg->getFileUrl() << ", skipping";
}
}
void MessageLine::setMyName(const QString& name)
{
myName = name;
for (Index::const_iterator itr = myMessages.begin(), end = myMessages.end(); itr != end; ++itr) {
itr->second->setSender(name);
}
}
void MessageLine::setPalName(const QString& jid, const QString& name)
{
std::map<QString, QString>::iterator itr = palNames.find(jid);
if (itr == palNames.end()) {
palNames.insert(std::make_pair(jid, name));
} else {
itr->second = name;
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setSender(name);
}
}
}
void MessageLine::setPalAvatar(const QString& jid, const QString& path)
{
std::map<QString, QString>::iterator itr = palAvatars.find(jid);
if (itr == palAvatars.end()) {
palAvatars.insert(std::make_pair(jid, path));
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(jid);
if (eitr != exPalAvatars.end()) {
exPalAvatars.erase(eitr);
}
} else {
itr->second = path;
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setAvatarPath(path);
}
}
}
void MessageLine::dropPalAvatar(const QString& jid)
{
std::map<QString, QString>::iterator itr = palAvatars.find(jid);
if (itr != palAvatars.end()) {
palAvatars.erase(itr);
}
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(jid);
if (eitr != exPalAvatars.end()) {
exPalAvatars.erase(eitr);
}
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) {
itr->second->setAvatarPath("");
}
}
}
void MessageLine::movePalAvatarToEx(const QString& name)
{
std::map<QString, QString>::iterator itr = palAvatars.find(name);
if (itr != palAvatars.end()) {
std::map<QString, QString>::iterator eitr = exPalAvatars.find(name);
if (eitr != exPalAvatars.end()) {
eitr->second = itr->second;
} else {
exPalAvatars.insert(std::make_pair(name, itr->second));
}
palAvatars.erase(itr);
}
}
void MessageLine::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
emit resize(event->size().height() - event->oldSize().height());
}
QString MessageLine::firstMessageId() const
{
if (messageOrder.size() == 0) {
return "";
} else {
return messageOrder.begin()->second->getId();
}
}
void MessageLine::showBusyIndicator()
{
if (!busyShown) {
layout->insertWidget(0, &progress);
progress.start();
busyShown = true;
}
}
void MessageLine::hideBusyIndicator()
{
if (busyShown) {
progress.stop();
layout->removeWidget(&progress);
busyShown = false;
}
}
void MessageLine::fileProgress(const QString& messageId, qreal progress)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr == messageIndex.end()) {
//TODO may be some logging, that's not normal
} else {
itr->second->setProgress(progress);
}
}
void MessageLine::responseLocalFile(const QString& messageId, const QString& path)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr == messageIndex.end()) {
} else {
Index::const_iterator uItr = uploading.find(messageId);
if (path.size() > 0) {
Index::const_iterator dItr = downloading.find(messageId);
if (dItr != downloading.end()) {
downloading.erase(dItr);
itr->second->showFile(path);
} else {
if (uItr != uploading.end()) {
uploading.erase(uItr);
std::map<QString, QString>::const_iterator muItr = uploadPaths.find(messageId);
if (muItr != uploadPaths.end()) {
uploadPaths.erase(muItr);
}
Shared::Message msg = itr->second->getMessage();
removeMessage(messageId);
msg.setCurrentTime();
message(msg);
itr = messageIndex.find(messageId);
itr->second->showFile(path);
} else {
itr->second->showFile(path); //then it is already cached file
}
}
} else {
if (uItr == uploading.end()) {
const Shared::Message& msg = itr->second->getMessage();
itr->second->addButton(QIcon::fromTheme("download"), tr("Download"), "<a href=\"" + msg.getOutOfBandUrl() + "\">" + msg.getOutOfBandUrl() + "</a>");
itr->second->showComment(tr("Push the button to download the file"));
} else {
qDebug() << "An unhandled state for file uploading - empty path";
}
}
}
}
void MessageLine::removeMessage(const QString& messageId)
{
Index::const_iterator itr = messageIndex.find(messageId);
if (itr != messageIndex.end()) {
Message* ui = itr->second;
const Shared::Message& msg = ui->getMessage();
messageIndex.erase(itr);
Order::const_iterator oItr = messageOrder.find(msg.getTime());
if (oItr != messageOrder.end()) {
messageOrder.erase(oItr);
} else {
qDebug() << "An attempt to remove message from messageLine, but it wasn't found in order";
}
if (msg.getOutgoing()) {
Index::const_iterator mItr = myMessages.find(messageId);
if (mItr != myMessages.end()) {
myMessages.erase(mItr);
} else {
qDebug() << "Error removing message: it seems to be outgoing yet it wasn't found in outgoing messages";
}
} else {
if (room) {
} else {
QString jid = msg.getFromJid();
std::map<QString, Index>::iterator pItr = palMessages.find(jid);
if (pItr != palMessages.end()) {
Index& pMsgs = pItr->second;
Index::const_iterator pmitr = pMsgs.find(messageId);
if (pmitr != pMsgs.end()) {
pMsgs.erase(pmitr);
} else {
qDebug() << "Error removing message: it seems to be incoming yet it wasn't found among messages from that penpal";
}
}
}
}
ui->deleteLater();
qDebug() << "message" << messageId << "has been removed";
} else {
qDebug() << "An attempt to remove non existing message from messageLine";
}
}
void MessageLine::fileError(const QString& messageId, const QString& error)
{
Index::const_iterator itr = downloading.find(messageId);
if (itr == downloading.end()) {
Index::const_iterator itr = uploading.find(messageId);
if (itr == uploading.end()) {
//TODO may be some logging, that's not normal
} else {
itr->second->showComment(tr("Error uploading file: %1\nYou can try again").arg(QCoreApplication::translate("NetworkErrors", error.toLatin1())), true);
itr->second->addButton(QIcon::fromTheme("upload"), tr("Upload"));
}
} else {
const Shared::Message& msg = itr->second->getMessage();
itr->second->addButton(QIcon::fromTheme("download"), tr("Download"), "<a href=\"" + msg.getOutOfBandUrl() + "\">" + msg.getOutOfBandUrl() + "</a>");
itr->second->showComment(tr("Error downloading file: %1\nYou can try again").arg(QCoreApplication::translate("NetworkErrors", error.toLatin1())), true);
}
}
void MessageLine::appendMessageWithUpload(const Shared::Message& msg, const QString& path)
{
appendMessageWithUploadNoSiganl(msg, path);
emit uploadFile(msg, path);
}
void MessageLine::appendMessageWithUploadNoSiganl(const Shared::Message& msg, const QString& path)
{
message(msg, true);
QString id = msg.getId();
Message* ui = messageIndex.find(id)->second;
connect(ui, &Message::buttonClicked, this, &MessageLine::onUpload); //this is in case of retry;
ui->setProgress(0);
ui->showComment(tr("Uploading..."));
uploading.insert(std::make_pair(id, ui));
uploadPaths.insert(std::make_pair(id, path));
}
void MessageLine::onUpload()
{
//TODO retry
}
void MessageLine::setMyAvatarPath(const QString& p_path)
{
if (myAvatarPath != p_path) {
myAvatarPath = p_path;
for (std::pair<QString, Message*> pair : myMessages) {
pair.second->setAvatarPath(myAvatarPath);
}
}
}
void MessageLine::setExPalAvatars(const std::map<QString, QString>& data)
{
exPalAvatars = data;
for (const std::pair<QString, Index>& pair : palMessages) {
if (palAvatars.find(pair.first) == palAvatars.end()) {
std::map<QString, QString>::const_iterator eitr = exPalAvatars.find(pair.first);
if (eitr != exPalAvatars.end()) {
for (const std::pair<QString, Message*>& mp : pair.second) {
mp.second->setAvatarPath(eitr->second);
}
}
}
}
}

View file

@ -1,108 +0,0 @@
/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MESSAGELINE_H
#define MESSAGELINE_H
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QResizeEvent>
#include <QIcon>
#include "shared/message.h"
#include "message.h"
#include "progress.h"
class MessageLine : public QWidget
{
Q_OBJECT
public:
enum Position {
beggining,
middle,
end,
invalid
};
MessageLine(bool p_room, QWidget* parent = 0);
~MessageLine();
Position message(const Shared::Message& msg, bool forceOutgoing = false);
void setMyName(const QString& name);
void setPalName(const QString& jid, const QString& name);
QString firstMessageId() const;
void showBusyIndicator();
void hideBusyIndicator();
void responseLocalFile(const QString& messageId, const QString& path);
void fileError(const QString& messageId, const QString& error);
void fileProgress(const QString& messageId, qreal progress);
void appendMessageWithUpload(const Shared::Message& msg, const QString& path);
void appendMessageWithUploadNoSiganl(const Shared::Message& msg, const QString& path);
void removeMessage(const QString& messageId);
void setMyAvatarPath(const QString& p_path);
void setPalAvatar(const QString& jid, const QString& path);
void dropPalAvatar(const QString& jid);
void changeMessage(const QString& id, const QMap<QString, QVariant>& data);
void setExPalAvatars(const std::map<QString, QString>& data);
void movePalAvatarToEx(const QString& name);
signals:
void resize(int amount);
void downloadFile(const QString& messageId, const QString& url);
void uploadFile(const Shared::Message& msg, const QString& path);
void requestLocalFile(const QString& messageId, const QString& url);
protected:
void resizeEvent(QResizeEvent * event) override;
protected:
void onDownload();
void onUpload();
private:
struct Comparator {
bool operator()(const Shared::Message& a, const Shared::Message& b) const {
return a.getTime() < b.getTime();
}
bool operator()(const Shared::Message* a, const Shared::Message* b) const {
return a->getTime() < b->getTime();
}
};
typedef std::map<QDateTime, Message*> Order;
typedef std::map<QString, Message*> Index;
Index messageIndex;
Order messageOrder;
Index myMessages;
std::map<QString, Index> palMessages;
std::map<QString, QString> uploadPaths;
std::map<QString, QString> palAvatars;
std::map<QString, QString> exPalAvatars;
QVBoxLayout* layout;
QString myName;
QString myAvatarPath;
std::map<QString, QString> palNames;
Index uploading;
Index downloading;
bool room;
bool busyShown;
Progress progress;
};
#endif // MESSAGELINE_H