conversationitemdelegate.cpp 17.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
/***************************************************************************
 * Copyright (C) 2015-2017 by Savoir-faire Linux                           *
 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
 *                                                                         *
 * 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 "conversationitemdelegate.h"

#include <QApplication>
#include <QPainter>
#include <QPixmap>
25
#include <QSvgRenderer>
26 27 28 29 30

// Client
#include "smartlistmodel.h"
#include "ringthemeutils.h"
#include "utils.h"
31
#include "lrcinstance.h"
32
#include "mainwindow.h"
33 34 35

#include <ciso646>

36 37
ConversationItemDelegate::ConversationItemDelegate(QObject* parent)
    : QItemDelegate(parent)
38
{
39 40 41 42 43
    QSvgRenderer svgRenderer(QString(":/images/icons/ic_baseline-search-24px.svg"));
    searchIcon_ = new QPixmap(QSize(sizeImage_, sizeImage_));
    searchIcon_->fill(Qt::transparent);
    QPainter pixPainter(searchIcon_);
    svgRenderer.render(&pixPainter);
44 45 46 47 48 49 50 51 52
}

void
ConversationItemDelegate::paint(QPainter* painter
                        , const QStyleOptionViewItem& option
                        , const QModelIndex& index
                        ) const
{
    QStyleOptionViewItem opt(option);
53
    painter->setRenderHint(QPainter::Antialiasing, true);
54 55 56 57 58 59 60 61 62 63 64

    // Not having focus removes dotted lines around the item
    if (opt.state & QStyle::State_HasFocus)
        opt.state ^= QStyle::State_HasFocus;

    auto isContextMenuOpen = index.data(static_cast<int>(SmartListModel::Role::ContextMenuOpen)).value<bool>();
    bool selected = false;
    if (option.state & QStyle::State_Selected) {
        selected = true;
        opt.state ^= QStyle::State_Selected;
    } else if (!isContextMenuOpen) {
65
        highlightMap_[index.row()] = option.state & QStyle::State_MouseOver;
66 67
    }

68 69 70 71 72
    using namespace lrc::api;
    auto type = Utils::toEnum<profile::Type>(
            index.data(static_cast<int>(SmartListModel::Role::ContactType)).value<int>()
        );

73 74
    // One does not simply keep the highlighted state drawn when the context
    // menu is open
75 76 77 78 79 80 81 82 83
    QString uriStr = index.data(static_cast<int>(SmartListModel::Role::URI)).value<QString>();
    if (not (type == profile::Type::TEMPORARY and uriStr.isEmpty())) {
        auto rowHighlight = highlightMap_.find(index.row());
        if (selected) {
            painter->fillRect(option.rect, RingTheme::smartlistSelection_);
        } else if (rowHighlight != highlightMap_.end() && (*rowHighlight).second) {
            painter->fillRect(option.rect, RingTheme::smartlistHighlight_);
        }
        auto convUid = index.data(static_cast<int>(SmartListModel::Role::UID)).value<QString>().toStdString();
84 85 86
        auto conversation = LRCInstance::getConversationFromConvUid(convUid);
        if (conversation.uid.empty()) return;
        if (LRCInstance::getCurrentCallModel()->hasCall(conversation.callId)) {
87 88 89 90
            auto color = QColor(RingTheme::blue_.lighter(180));
            color.setAlpha(128);
            painter->fillRect(option.rect, color);
        }
91 92 93 94 95 96 97 98
    }

    QRect &rect = opt.rect;

    opt.decorationSize = QSize(sizeImage_, sizeImage_);
    opt.decorationPosition = QStyleOptionViewItem::Left;
    opt.decorationAlignment = Qt::AlignCenter;
    QRect rectAvatar(dx_ + rect.left(), rect.top() + dy_, sizeImage_, sizeImage_);
99 100 101 102 103 104 105 106 107
    if (type == profile::Type::TEMPORARY and uriStr.isEmpty()) {
        // Search icon
        drawDecoration(painter, opt, rectAvatar, *searchIcon_);
    } else {
        // Avatar drawing
        drawDecoration(painter, opt, rectAvatar,
            QPixmap::fromImage(index.data(Qt::DecorationRole).value<QImage>())
            .scaled(sizeImage_, sizeImage_, Qt::KeepAspectRatio, Qt::SmoothTransformation));
    }
108 109 110

    QFont font(painter->font());

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    if (type != profile::Type::TEMPORARY) {
        // If there's unread messages, a message count is displayed
        if (auto messageCount = index.data(static_cast<int>(SmartListModel::Role::UnreadMessagesCount)).toInt()) {
            QString messageCountText = (messageCount > 9) ? "9+" : QString::number(messageCount);
            qreal fontSize = messageCountText.count() > 1 ? 7 : 8;
            font.setPointSize(fontSize);

            // ellipse
            QPainterPath ellipse;
            qreal ellipseHeight = sizeImage_ / 6;
            qreal ellipseWidth = ellipseHeight;
            QPointF ellipseCenter(rectAvatar.right() - ellipseWidth + 1, rectAvatar.top() + ellipseHeight + 1);
            QRect ellipseRect(ellipseCenter.x() - ellipseWidth, ellipseCenter.y() - ellipseHeight,
                ellipseWidth * 2, ellipseHeight * 2);
            ellipse.addRoundedRect(ellipseRect, ellipseWidth, ellipseHeight);
            painter->fillPath(ellipse, RingTheme::notificationRed_);

            // text
            painter->setPen(Qt::white);
            painter->setOpacity(1);
            painter->setFont(font);
            ellipseRect.setTop(ellipseRect.top() - 2);
            painter->drawText(ellipseRect, Qt::AlignCenter, messageCountText);
        }
135

136 137 138 139 140 141 142 143 144 145 146 147
        // Presence indicator
        if (index.data(static_cast<int>(SmartListModel::Role::Presence)).value<bool>()) {
            qreal radius = sizeImage_ / 6;
            QPainterPath outerCircle, innerCircle;
            QPointF center(rectAvatar.right() - radius + 2, (rectAvatar.bottom() - radius) + 1 + 2);
            qreal outerCRadius = radius;
            qreal innerCRadius = outerCRadius * 0.75;
            outerCircle.addEllipse(center, outerCRadius, outerCRadius);
            innerCircle.addEllipse(center, innerCRadius, innerCRadius);
            painter->fillPath(outerCircle, Qt::white);
            painter->fillPath(innerCircle, RingTheme::presenceGreen_);
        }
148 149 150
    }

    switch (type) {
151
    case profile::Type::TEMPORARY:
152
    case profile::Type::RING:
153
    case profile::Type::SIP:
154 155
        paintConversationItem(painter, option, rect, index,
                              type == profile::Type::TEMPORARY);
156 157
        break;
    case profile::Type::PENDING:
158
        paintInvitationItem(painter, option, rect, index);
159 160
        break;
    default:
161
        paintConversationItem(painter, option, rect, index, true);
162 163 164 165 166 167
        break;
    }
}

QSize
ConversationItemDelegate::sizeHint(const QStyleOptionViewItem& option,
168
                                   const QModelIndex& index) const
169
{
170 171
    Q_UNUSED(option);
    Q_UNUSED(index);
172 173 174 175
    return QSize(0, cellHeight_);
}

void
176 177 178
ConversationItemDelegate::paintConversationItem(QPainter* painter,
                                                const QStyleOptionViewItem& option,
                                                const QRect& rect,
179 180
                                                const QModelIndex& index,
                                                const bool isTemporary) const
181
{
182
    Q_UNUSED(option);
183 184 185 186
    QFont font(painter->font());
    QPen pen(painter->pen());
    painter->setPen(pen);

187
    int infoTextWidthModifier = 0;
188
    int infoText2HeightModifier = 0;
189 190
    auto scalingRatio = MainWindow::instance().getCurrentScalingRatio();
    if (scalingRatio > 1.0) {
191
        font.setPointSize(fontSize_ - 2);
192
        infoTextWidthModifier = 12;
193 194 195
        infoText2HeightModifier = 2;
    } else {
        font.setPointSize(fontSize_);
196
        infoTextWidthModifier = 10;
197 198 199
    }

    auto leftMargin = dx_ + sizeImage_ + dx_;
200
    auto rightMargin = dx_;
201 202
    auto topMargin = 4;
    auto bottomMargin = 8;
203

204 205 206 207 208 209 210
    int rect1Width;
    if (!isTemporary) {
        rect1Width = rect.width() - leftMargin - infoTextWidth_ - infoTextWidthModifier - 8;
    } else {
        rect1Width = rect.width() - leftMargin - rightMargin;
    }

211 212
    QRect rectName1(rect.left() + leftMargin,
                    rect.top() + topMargin,
213
                    rect1Width,
214
                    rect.height() / 2 - 2);
215 216

    QRect rectName2(rectName1.left(),
217
                    rectName1.top() + rectName1.height() - infoText2HeightModifier,
218
                    rectName1.width(),
219
                    rectName1.height() - bottomMargin + infoText2HeightModifier);
220 221 222 223

    QFontMetrics fontMetrics(font);

    // The name is displayed at the avatar's right
224
    QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>();
225 226 227
    if (!nameStr.isNull()) {
        pen.setColor(RingTheme::lightBlack_);
        painter->setPen(pen);
228 229 230 231
        QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji"));
        emojiMsgFont.setPointSize(scalingRatio > 1.0 ? fontSize_ - 2 : fontSize_);
        QString elidedNameStr = QFontMetrics(emojiMsgFont).elidedText(nameStr, Qt::ElideRight, rectName1.width());
        painter->setFont(emojiMsgFont);
232
        painter->drawText(rectName1, Qt::AlignVCenter | Qt::AlignLeft, elidedNameStr);
233
        painter->setFont(font);
234 235 236 237 238 239 240 241 242 243 244 245 246 247
    }

    // Display the ID under the name
    QString idStr = index.data(static_cast<int>(SmartListModel::Role::DisplayID)).value<QString>();
    if (idStr != nameStr && !idStr.isNull()) {
        font.setItalic(false);
        font.setBold(false);
        pen.setColor(RingTheme::grey_);
        painter->setPen(pen);
        painter->setFont(font);
        idStr = fontMetrics.elidedText(idStr, Qt::ElideRight, rectName2.width());
        painter->drawText(rectName2, Qt::AlignVCenter | Qt::AlignLeft, idStr);
    }

248 249 250 251 252 253 254 255 256 257 258 259 260 261
    if (isTemporary) {
        return;
    }

    QRect rectInfo1(rectName1.left() + rectName1.width(),
                    rect.top() + topMargin,
                    infoTextWidth_ - rightMargin + infoTextWidthModifier + 2,
                    rect.height() / 2 - 2);

    QRect rectInfo2(rectInfo1.left(),
                    rectInfo1.top() + rectInfo1.height() - infoText2HeightModifier,
                    rectInfo1.width(),
                    rectInfo1.height() - bottomMargin + infoText2HeightModifier + 4);

262 263 264 265 266 267 268 269 270 271 272 273
    // top-right: last interaction date/time
    QString lastUsedStr = index.data(static_cast<int>(SmartListModel::Role::LastInteractionDate)).value<QString>();
    if (!lastUsedStr.isNull()) {
        font.setItalic(false);
        font.setBold(false);
        pen.setColor(RingTheme::grey_);
        painter->setPen(pen);
        painter->setFont(font);
        lastUsedStr = fontMetrics.elidedText(lastUsedStr, Qt::ElideRight, rectInfo1.width());
        painter->drawText(rectInfo1, Qt::AlignVCenter | Qt::AlignRight, lastUsedStr);
    }

274
    // bottom-right: last interaction snippet or call state (if in call)
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
    auto draft = index.data(static_cast<int>(SmartListModel::Role::Draft)).value<QString>();
    if (!draft.isEmpty()) {
        painter->save();
        uint cp = 0x270F;
        auto emojiString = QString::fromUcs4(&cp, 1);
        QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji"));
        emojiMsgFont.setPointSize(scalingRatio > 1.0 ? fontSize_ - 2 : fontSize_);

        QString text{ tr("Draft") };
        int widthText{ QFontMetrics(font).width(text) };
        int widthEmoji{ QFontMetrics(emojiMsgFont).width(emojiString) };
        int margin { 4 };

        painter->setFont(font);
        painter->setPen(QColor(207, 83, 0));
        painter->drawText(rectInfo2, Qt::AlignVCenter | Qt::AlignRight, text);

        rectInfo2.moveTop(rectInfo2.top() - 2);
        painter->setOpacity(0.7);
        painter->setFont(emojiMsgFont);
        painter->drawText(rectInfo2.right() - widthText - widthEmoji - margin, rectInfo2.y(),
                          widthEmoji, rectInfo2.height(), Qt::AlignVCenter, emojiString);
        painter->restore();
        return;
    }

301 302 303 304 305
    if (index.data(static_cast<int>(SmartListModel::Role::InCall)).value<bool>()) {
        QString callStateStr = index.data(static_cast<int>(SmartListModel::Role::CallStateStr)).value<QString>();
        if (!callStateStr.isNull()) {
            painter->save();
            font.setWeight(QFont::DemiBold);
306 307 308
            pen.setColor(RingTheme::grey_.darker(140));
            painter->setPen(pen);
            painter->setFont(font);
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
            callStateStr = fontMetrics.elidedText(callStateStr, Qt::ElideRight, rectInfo2.width());
            painter->drawText(rectInfo2, Qt::AlignVCenter | Qt::AlignRight, callStateStr);
            painter->restore();
        }
    } else {
        QString interactionStr = index.data(static_cast<int>(SmartListModel::Role::LastInteraction)).value<QString>();
        if (!interactionStr.isNull()) {
            painter->save();
            font.setWeight(QFont::ExtraLight);
            interactionStr = interactionStr.simplified();
            auto type = Utils::toEnum<lrc::api::interaction::Type>(index
                .data(static_cast<int>(SmartListModel::Role::LastInteractionType))
                .value<int>());
            if (type == lrc::api::interaction::Type::CALL ||
                type == lrc::api::interaction::Type::CONTACT) {
                font.setItalic(false);
                font.setBold(false);
                pen.setColor(RingTheme::grey_.darker(140));
                painter->setPen(pen);
                painter->setFont(font);
                // strip emojis if it's a call/contact type message
                VectorUInt emojiless;
                for (auto unicode : interactionStr.toUcs4()) {
                    if (!(unicode >= 0x1F000 && unicode <= 0x1FFFF)) {
                        emojiless.push_back(unicode);
                    }
335
                }
336
                interactionStr = QString::fromUcs4(&emojiless.at(0), emojiless.size());
337
            } else {
338 339 340 341 342
                QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji"));
                emojiMsgFont.setPointSize(scalingRatio > 1.0 ? fontSize_ - 2 : fontSize_);
                rectInfo2.setTop(rectInfo2.top() - 6);
                painter->setOpacity(0.7);
                painter->setFont(emojiMsgFont);
343
            }
344 345 346
            interactionStr = fontMetrics.elidedText(interactionStr, Qt::ElideRight, rectInfo2.width());
            painter->drawText(rectInfo2, Qt::AlignVCenter | Qt::AlignRight, interactionStr);
            painter->restore();
347
        }
348 349 350 351
    }
}

void
352 353 354 355
ConversationItemDelegate::paintInvitationItem(QPainter* painter,
                                              const QStyleOptionViewItem& option,
                                              const QRect& rect,
                                              const QModelIndex& index) const
356 357 358 359 360
{
    QFont font(painter->font());
    QPen pen(painter->pen());
    painter->setPen(pen);

361
    auto leftMargin = dx_ + sizeImage_ + dx_;
362
    auto rightMargin = dx_;
363 364 365 366

    auto scalingRatio = MainWindow::instance().getCurrentScalingRatio();
    font.setPointSize(scalingRatio > 1.0 ? fontSize_ - 2 : fontSize_);

367
    if (option.state & QStyle::State_MouseOver) {
368 369 370 371 372
        if (scalingRatio > 1.0) {
            rightMargin = infoTextWidth_ + 12 - dx_ * 2;
        } else {
            rightMargin = infoTextWidth_ - dx_ * 2;
        }
373
    }
374

375 376
    auto topMargin = 4;
    auto bottomMargin = 8;
377 378 379 380

    QRect rectName1(rect.left() + leftMargin,
        rect.top() + topMargin,
        rect.width() - leftMargin - rightMargin,
381
        rect.height() / 2 - 2);
382 383 384 385 386 387 388 389 390

    QRect rectName2(rectName1.left(),
        rectName1.top() + rectName1.height(),
        rectName1.width(),
        rectName1.height() - bottomMargin);

    QFontMetrics fontMetrics(font);

    // The name is displayed at the avatar's right
391
    QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>();
392 393 394
    if (!nameStr.isNull()) {
        pen.setColor(RingTheme::lightBlack_);
        painter->setPen(pen);
395 396 397 398
        QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji"));
        emojiMsgFont.setPointSize(scalingRatio > 1.0 ? fontSize_ - 2 : fontSize_);
        QString elidedNameStr = QFontMetrics(emojiMsgFont).elidedText(nameStr, Qt::ElideRight, rectName1.width());
        painter->setFont(emojiMsgFont);
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
        painter->drawText(rectName1, Qt::AlignVCenter | Qt::AlignLeft, elidedNameStr);
    }

    // Display the ID under the name
    QString idStr = index.data(static_cast<int>(SmartListModel::Role::DisplayID)).value<QString>();
    if (idStr != nameStr && !idStr.isNull()) {
        font.setItalic(false);
        font.setBold(false);
        pen.setColor(RingTheme::grey_);
        painter->setPen(pen);
        painter->setFont(font);
        idStr = fontMetrics.elidedText(idStr, Qt::ElideRight, rectName2.width());
        painter->drawText(rectName2, Qt::AlignVCenter | Qt::AlignLeft, idStr);
    }
}