Skip to content
Snippets Groups Projects
RoomInfoListItem.cpp 14.8 KiB
Newer Older
Konstantinos Sideris's avatar
Konstantinos Sideris committed
/*
 * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
 *
 * 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 <QDateTime>
#include <QDebug>
Konstantinos Sideris's avatar
Konstantinos Sideris committed
#include <QMouseEvent>
#include <QPainter>
Konstantinos Sideris's avatar
Konstantinos Sideris committed
#include "Cache.h"
#include "Config.h"
Konstantinos Sideris's avatar
Konstantinos Sideris committed
#include "RoomInfoListItem.h"
#include "Utils.h"
#include "ui/Menu.h"
#include "ui/Ripple.h"
#include "ui/RippleOverlay.h"
constexpr int MaxUnreadCountDisplayed = 99;
constexpr int IconSize = 44;
// constexpr int MaxHeight        = IconSize + 2 * Padding;
struct WidgetMetrics
{
        int maxHeight;
        int iconSize;
        int padding;
        int unit;

        int unreadLineWidth;
        int unreadLineOffset;

        int inviteBtnX;
        int inviteBtnY;
};

WidgetMetrics
getMetrics(const QFont &font)
{
        WidgetMetrics m;

        const int height = QFontMetrics(font).lineSpacing();

        m.unit             = height;
        m.maxHeight        = std::ceil((double)height * 3.8);
        m.iconSize         = std::ceil((double)height * 2.8);
        m.padding          = std::ceil((double)height / 2.0);
        m.unreadLineWidth  = m.padding - m.padding / 3;
        m.unreadLineOffset = m.padding - m.padding / 4;

        m.inviteBtnX = m.iconSize + 2 * m.padding;
        m.inviteBtnX = m.iconSize / 2.0 + m.padding + m.padding / 3.0;

        return m;
}

void
RoomInfoListItem::init(QWidget *parent)
Konstantinos Sideris's avatar
Konstantinos Sideris committed
{
        setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
        setMouseTracking(true);
        setAttribute(Qt::WA_Hover);
        setFixedHeight(getMetrics(QFont{}).maxHeight);
        QPainterPath path;
        path.addRect(0, 0, parent->width(), height());
        ripple_overlay_ = new RippleOverlay(this);
        ripple_overlay_->setClipPath(path);
        ripple_overlay_->setClipping(true);
        unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8);
        unreadCountFont_.setBold(true);
        bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3;
Konstantinos Sideris's avatar
Konstantinos Sideris committed
        menu_      = new Menu(this);
        leaveRoom_ = new QAction(tr("Leave room"), this);
        connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); });
        menu_->addAction(leaveRoom_);
Konstantinos Sideris's avatar
Konstantinos Sideris committed
RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent)
  : QWidget(parent)
Konstantinos Sideris's avatar
Konstantinos Sideris committed
  , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined}
  , roomId_(std::move(room_id))
  , roomName_{QString::fromStdString(std::move(info.name))}
  , isPressed_(false)
  , unreadMsgCount_(0)
  , unreadHighlightedMsgCount_(0)
        // HACK
        // We use fake message info with an old date to pin
        // the invite events to the top.
        //
        // State events in invited rooms don't contain timestamp info,
        // so we can't use them for sorting.
Konstantinos Sideris's avatar
Konstantinos Sideris committed
        if (roomType_ == RoomType::Invited)
                lastMsgInfo_ = {
                  emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
void
RoomInfoListItem::resizeEvent(QResizeEvent *)
        // Update ripple's clipping path.
        QPainterPath path;
        path.addRect(0, 0, width(), height());
        const auto sidebarSizes = utils::calculateSidebarSizes(QFont{});

        if (width() > sidebarSizes.small)
                setToolTip("");
        else
                setToolTip(roomName_);

        ripple_overlay_->setClipPath(path);
        ripple_overlay_->setClipping(true);
void
RoomInfoListItem::paintEvent(QPaintEvent *event)
        Q_UNUSED(event);

        QPainter p(this);
        p.setRenderHint(QPainter::TextAntialiasing);
        p.setRenderHint(QPainter::SmoothPixmapTransform);
        p.setRenderHint(QPainter::Antialiasing);

        QFontMetrics metrics(QFont{});
        QPen titlePen(titleColor_);
        QPen subtitlePen(subtitleColor_);

        auto wm = getMetrics(QFont{});

        if (isPressed_) {
                p.fillRect(rect(), highlightedBackgroundColor_);
                titlePen.setColor(highlightedTitleColor_);
                subtitlePen.setColor(highlightedSubtitleColor_);
        } else if (underMouse()) {
                p.fillRect(rect(), hoverBackgroundColor_);
                titlePen.setColor(hoverTitleColor_);
                subtitlePen.setColor(hoverSubtitleColor_);
        } else {
                p.fillRect(rect(), backgroundColor_);
                titlePen.setColor(titleColor_);
                subtitlePen.setColor(subtitleColor_);
        QRect avatarRegion(wm.padding, wm.padding, wm.iconSize, wm.iconSize);

        // Description line with the default font.
        int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2;
        const auto sidebarSizes = utils::calculateSidebarSizes(QFont{});

        if (width() > sidebarSizes.small) {
                QFont headingFont;
                headingFont.setWeight(QFont::Medium);
                p.setFont(headingFont);
                p.setPen(titlePen);
                QFont tsFont;
                tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9);
                const int msgStampWidth = QFontMetrics(tsFont).width(lastMsgInfo_.timestamp) + 4;
                // We use the full width of the widget if there is no unread msg bubble.
                const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0;

                // Name line.
                QFontMetrics fontNameMetrics(headingFont);
                int top_y = 2 * wm.padding + fontNameMetrics.ascent() / 2;
                const auto name = metrics.elidedText(
                  roomName(),
                  Qt::ElideRight,
                  (width() - wm.iconSize - 2 * wm.padding - msgStampWidth) * 0.8);
                p.drawText(QPoint(2 * wm.padding + wm.iconSize, top_y), name);
                if (roomType_ == RoomType::Joined) {
                        p.setPen(subtitlePen);
                        // The limit is the space between the end of the avatar and the start of the
                        // timestamp.
                        int usernameLimit =
                          std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20);
                        auto userName =
                          metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit);
                        p.setFont(QFont{});
                        p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName);
                        int nameWidth = QFontMetrics(QFont{}).width(userName);

                        // The limit is the space between the end of the username and the start of
                        // the timestamp.
                        int descriptionLimit =
                          std::max(0,
                                   width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize -
                                     nameWidth - 5);
                        auto description =
                          metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
                        p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y),
                                   description);

                        // We show the last message timestamp.
                        p.save();
                        if (isPressed_) {
                                p.setPen(QPen(highlightedTimestampColor_));
                        } else if (underMouse()) {
                                p.setPen(QPen(hoverTimestampColor_));
                        } else {
                                p.setPen(QPen(timestampColor_));
                        p.drawText(QPoint(width() - wm.padding - msgStampWidth, top_y),
                                   lastMsgInfo_.timestamp);
                        p.restore();
                } else {
                        int btnWidth = (width() - wm.iconSize - 6 * wm.padding) / 2;
                        acceptBtnRegion_  = QRectF(wm.inviteBtnX, wm.inviteBtnY, btnWidth, 20);
                        declineBtnRegion_ = QRectF(
                          wm.inviteBtnX + btnWidth + 2 * wm.padding, wm.inviteBtnY, btnWidth, 20);
                        QPainterPath acceptPath;
                        acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10);
                        p.setPen(Qt::NoPen);
                        p.fillPath(acceptPath, btnColor_);
                        p.drawPath(acceptPath);
                        QPainterPath declinePath;
                        declinePath.addRoundedRect(declineBtnRegion_, 10, 10);
                        p.setPen(Qt::NoPen);
                        p.fillPath(declinePath, btnColor_);
                        p.drawPath(declinePath);

                        p.setPen(QPen(btnTextColor_));
                        p.drawText(acceptBtnRegion_, Qt::AlignCenter, tr("Accept"));
                        p.drawText(declineBtnRegion_, Qt::AlignCenter, tr("Decline"));
        p.setPen(Qt::NoPen);
        // We using the first letter of room's name.
        if (roomAvatar_.isNull()) {
                QBrush brush;
                brush.setStyle(Qt::SolidPattern);
                brush.setColor(avatarBgColor());
                p.setPen(Qt::NoPen);
                p.setBrush(brush);
                p.drawEllipse(avatarRegion.center(), wm.iconSize / 2, wm.iconSize / 2);
                QFont bubbleFont;
                bubbleFont.setPointSizeF(bubbleFont.pointSizeF() * 1.4);
                p.setFont(bubbleFont);
                p.setPen(avatarFgColor());
                p.setBrush(Qt::NoBrush);
                p.drawText(
                  avatarRegion.translated(0, -1), Qt::AlignCenter, utils::firstChar(roomName()));
        } else {
                p.save();
                QPainterPath path;
                path.addEllipse(wm.padding, wm.padding, wm.iconSize, wm.iconSize);
                p.setClipPath(path);
                p.drawPixmap(avatarRegion, roomAvatar_);
                p.restore();
        }
        if (unreadMsgCount_ > 0) {
                QBrush brush;
                brush.setStyle(Qt::SolidPattern);
                if (unreadHighlightedMsgCount_ > 0) {
                        brush.setColor(mentionedColor());
                } else {
                        brush.setColor(bubbleBgColor());
                }

                if (isPressed_)
                        brush.setColor(bubbleFgColor());

                p.setBrush(brush);
                p.setPen(Qt::NoPen);
                p.setFont(unreadCountFont_);
                // Extra space on the x-axis to accomodate the extra character space
                // inside the bubble.
                const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed
                                      ? QFontMetrics(p.font()).averageCharWidth()
                                      : 0;

                QRectF r(width() - bubbleDiameter_ - wm.padding - x_width,
                         bottom_y - bubbleDiameter_ / 2 - 5,
                         bubbleDiameter_ + x_width,
                         bubbleDiameter_);
                if (width() == sidebarSizes.small)
                        r = QRectF(width() - bubbleDiameter_ - 5,
                                   height() - bubbleDiameter_ - 5,
                                   bubbleDiameter_ + x_width,
                                   bubbleDiameter_);

                p.setPen(Qt::NoPen);
                p.drawEllipse(r);
                p.setPen(QPen(bubbleFgColor()));

                if (isPressed_)
                        p.setPen(QPen(bubbleBgColor()));
                auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed
                                  ? QString("99+")
                                  : QString::number(unreadMsgCount_);

                p.setBrush(Qt::NoBrush);
                p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt);

        if (!isPressed_ && hasUnreadMessages_) {
                QPen pen;
                pen.setWidth(wm.unreadLineWidth);
                pen.setColor(highlightedBackgroundColor_);

                p.setPen(pen);
                p.drawLine(0, wm.unreadLineOffset, 0, height() - wm.unreadLineOffset);
void
RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount)
        unreadMsgCount_            = count;
        unreadHighlightedMsgCount_ = highlightedCount;
        update();
void
RoomInfoListItem::setPressedState(bool state)
Konstantinos Sideris's avatar
Konstantinos Sideris committed
{
Max Sandholm's avatar
Max Sandholm committed
        if (isPressed_ != state) {
                isPressed_ = state;
                update();
        }
void
RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event)
        Q_UNUSED(event);
        if (roomType_ == RoomType::Invited)
                return;

        menu_->popup(event->globalPos());
void
RoomInfoListItem::mousePressEvent(QMouseEvent *event)
Konstantinos Sideris's avatar
Konstantinos Sideris committed
{
        if (event->buttons() == Qt::RightButton) {
                QWidget::mousePressEvent(event);
                return;
        }
        if (roomType_ == RoomType::Invited) {
                const auto point = event->pos();

                if (acceptBtnRegion_.contains(point))
                        emit acceptInvite(roomId_);

                if (declineBtnRegion_.contains(point))
                        emit declineInvite(roomId_);

                return;
        }

        emit clicked(roomId_);
        setPressedState(true);
        // Ripple on mouse position by default.
        QPoint pos           = event->pos();
        qreal radiusEndValue = static_cast<qreal>(width()) / 3;
        Ripple *ripple = new Ripple(pos);
        ripple->setRadiusEndValue(radiusEndValue);
        ripple->setOpacityStartValue(0.15);
        ripple->setColor(QColor("white"));
        ripple->radiusAnimation()->setDuration(200);
        ripple->opacityAnimation()->setDuration(400);
        ripple_overlay_->addRipple(ripple);
void
RoomInfoListItem::setAvatar(const QImage &img)
{
        roomAvatar_ = utils::scaleImageToPixmap(img, IconSize);
        update();
}

void
RoomInfoListItem::setDescriptionMessage(const DescInfo &info)
{
        lastMsgInfo_ = info;
        update();
}