message preview refactor, several bugs about label size, animations are now playing in previews
This commit is contained in:
parent
4307262f6e
commit
0d584c5aba
20 changed files with 498 additions and 164 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
// {
|
||||
//
|
||||
// }
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue