223 lines
11 KiB
C++
223 lines
11 KiB
C++
#include "accountdelegate.h"
|
|
#include "flagpainter.h"
|
|
#include "memberlistmodel.h"
|
|
#include "debug_ui.h"
|
|
#include <QPainter>
|
|
#include <QFont>
|
|
#include <QMouseEvent>
|
|
#include <QHelpEvent>
|
|
#include <QToolTip>
|
|
#include <QPainterPath>
|
|
#include <QCoreApplication>
|
|
#include <QAbstractItemView>
|
|
|
|
extern "C" {
|
|
#include "chat/chat_member.h"
|
|
}
|
|
|
|
QRect AccountDelegate::actionRect(const QRect& row, Action action) {
|
|
const int offset = action == Action::RadioMute ? 95 : action == Action::RadioCall ? 63 : 31;
|
|
return QRect(row.right() - offset, row.center().y() - 14, 28, 28);
|
|
}
|
|
|
|
/* Трубка в той же системе координат 24×24, что и портативная рация. */
|
|
static void drawCallButton(QPainter* painter, const QRect& button, const QColor& color) {
|
|
painter->save();
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
painter->translate(button.center().x() - 12, button.center().y() - 12);
|
|
QPainterPath phone;
|
|
phone.moveTo(6.6, 10.8); phone.cubicTo(8.1, 13.6, 10.4, 15.9, 13.2, 17.4);
|
|
phone.lineTo(15.4, 15.2); phone.quadTo(15.8, 14.8, 16.3, 15);
|
|
phone.quadTo(18.1, 15.6, 20, 15.6); phone.quadTo(21, 15.6, 21, 16.6);
|
|
phone.lineTo(21, 20); phone.quadTo(21, 21, 20, 21);
|
|
phone.cubicTo(10.6, 21, 3, 13.4, 3, 4); phone.quadTo(3, 3, 4, 3);
|
|
phone.lineTo(7.4, 3); phone.quadTo(8.4, 3, 8.4, 4);
|
|
phone.quadTo(8.4, 5.9, 9, 7.7); phone.quadTo(9.2, 8.2, 8.8, 8.6);
|
|
phone.closeSubpath();
|
|
painter->fillPath(phone, color);
|
|
painter->restore();
|
|
}
|
|
|
|
// Волны показывают передачу независимо от локального mute; перечёркивание сохраняется при речи.
|
|
static void drawRadioMuteButton(QPainter* painter, const QRect& button, bool muted, bool speaking) {
|
|
painter->save();
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
painter->translate(button.center());
|
|
QColor color(muted ? "#EF5350" : speaking ? "#4CAF50" : "#9E9E9E");
|
|
QPainterPath speaker;
|
|
speaker.moveTo(-8, -3); speaker.lineTo(-4, -3); speaker.lineTo(1, -7);
|
|
speaker.lineTo(1, 7); speaker.lineTo(-4, 3); speaker.lineTo(-8, 3); speaker.closeSubpath();
|
|
painter->fillPath(speaker, color);
|
|
painter->setPen(QPen(color, 1.8, Qt::SolidLine, Qt::RoundCap)); painter->setBrush(Qt::NoBrush);
|
|
if (muted) painter->drawLine(QPointF(-10, 10), QPointF(10, -10));
|
|
if (speaking) {
|
|
painter->drawArc(QRectF(-3, -5, 10, 10), -60 * 16, 120 * 16);
|
|
painter->drawArc(QRectF(-6, -8, 16, 16), -60 * 16, 120 * 16);
|
|
}
|
|
painter->restore();
|
|
}
|
|
|
|
static QRect avatarRect(const QRect& row) {
|
|
const int size = MemberListModel::kAvatarSize;
|
|
return QRect(row.left() + 4, row.top() + (row.height() - size) / 2, size, size);
|
|
}
|
|
|
|
AccountDelegate::Action AccountDelegate::actionAt(const QPoint& position, const QRect& row, bool self) {
|
|
if (avatarRect(row).contains(position)) return Action::Avatar;
|
|
if (!self) for (Action action : {Action::RadioMute, Action::RadioCall, Action::Call})
|
|
if (actionRect(row, action).contains(position)) return action;
|
|
return Action::None;
|
|
}
|
|
|
|
AccountDelegate::AccountDelegate(QObject *parent)
|
|
: QStyledItemDelegate(parent) {
|
|
GUI_DEBUG("AccountDelegate: row height=%dpx avatar=%dpx gap=%dpx", MemberListModel::kRowHeight,
|
|
MemberListModel::kAvatarSize, MemberListModel::kRowHeight - MemberListModel::kAvatarSize);
|
|
}
|
|
|
|
void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const {
|
|
painter->save();
|
|
|
|
if (option.state & QStyle::State_Selected)
|
|
painter->fillRect(option.rect, option.palette.highlight());
|
|
else if (option.state & QStyle::State_MouseOver)
|
|
painter->fillRect(option.rect, option.palette.alternateBase().color().lighter(115));
|
|
else
|
|
painter->fillRect(option.rect, option.palette.alternateBase());
|
|
|
|
const int margin = 4;
|
|
const int iconSz = MemberListModel::kAvatarSize;
|
|
|
|
QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
|
|
if (!icon.isNull())
|
|
icon.paint(painter, avatarRect(option.rect));
|
|
|
|
const int x = option.rect.left() + iconSz + margin * 3;
|
|
const bool self = index.data(Qt::UserRole + 2).toBool();
|
|
const int w = qMax(0, option.rect.right() - x - margin - (self ? 32 : 96));
|
|
const int yMid = option.rect.top() + option.rect.height() / 2;
|
|
|
|
uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF);
|
|
|
|
QFont nameFont = painter->font();
|
|
nameFont.setBold(true);
|
|
nameFont.setPixelSize(13);
|
|
if (flags & MEMBER_FLAG_DELETED) nameFont.setStrikeOut(true);
|
|
painter->setFont(nameFont);
|
|
|
|
QColor textColor = option.palette.color(QPalette::Text);
|
|
if (option.state & QStyle::State_Selected) textColor = option.palette.color(QPalette::HighlightedText);
|
|
if (index.data(Qt::UserRole + 2).toBool()) textColor = QColor("#4CAF50");
|
|
painter->setPen(textColor);
|
|
|
|
painter->save();
|
|
painter->setClipRect(QRect(x, option.rect.top(), w, option.rect.height()), Qt::IntersectClip);
|
|
const uint8_t visualFlags = flags & (MEMBER_FLAG_SUPERNODE | MEMBER_FLAG_ADMIN | MEMBER_FLAG_MODER);
|
|
int flagX = x;
|
|
if (visualFlags) {
|
|
FlagPainter::draw(painter, flags, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize());
|
|
int n = __builtin_popcount(visualFlags);
|
|
flagX += nameFont.pixelSize() * n + (n - 1) * 2;
|
|
}
|
|
if (index.data(Qt::UserRole + 4).toBool()) {
|
|
FlagPainter::drawStorage(painter, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize());
|
|
flagX += nameFont.pixelSize() + 2;
|
|
}
|
|
QString name = index.data(Qt::DisplayRole).toString();
|
|
painter->drawText(QRect(flagX, option.rect.top() + 2, w - (flagX - x), yMid - option.rect.top() - 2),
|
|
Qt::AlignLeft | Qt::AlignBottom, name);
|
|
|
|
QFont subFont = painter->font();
|
|
subFont.setBold(false);
|
|
subFont.setPixelSize(10);
|
|
painter->setFont(subFont);
|
|
painter->setPen(QColor(130, 130, 130));
|
|
|
|
QString sub = index.data(Qt::UserRole + 1).toString();
|
|
if (!sub.isEmpty())
|
|
painter->drawText(QRect(flagX, yMid + 2, w - (flagX - x), option.rect.bottom() - yMid - 2),
|
|
Qt::AlignLeft | Qt::AlignTop, sub);
|
|
|
|
painter->restore();
|
|
drawRadioMuteButton(painter, actionRect(option.rect, self ? Action::Call : Action::RadioMute), flags & CHAT_MEMBER_FLAG_RADIO_MUTED,
|
|
index.data(MemberListModel::RadioSpeakingRole).toBool());
|
|
if (!self) {
|
|
const QColor color = index.data(MemberListModel::OnlineRole).toBool() ? textColor :
|
|
option.palette.color(QPalette::Disabled, QPalette::Text);
|
|
const QRect radio = actionRect(option.rect, Action::RadioCall);
|
|
const QColor radioColor = flags & CHAT_MEMBER_FLAG_RADIO ? QColor("#4CAF50") : color;
|
|
FlagPainter::drawRadio(painter, radio.center().x() - 12, radio.center().y() - 12, 24, radioColor);
|
|
const bool known = index.data(MemberListModel::OnlineRole).toBool() && (flags & CHAT_MEMBER_FLAG_PHONE_KNOWN);
|
|
const QColor phoneColor = known ? QColor(flags & CHAT_MEMBER_FLAG_PHONE_BUSY ? "#E53935" : "#4CAF50") : color;
|
|
drawCallButton(painter, actionRect(option.rect, Action::Call), phoneColor);
|
|
}
|
|
painter->restore();
|
|
}
|
|
|
|
bool AccountDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option,
|
|
const QModelIndex& index) {
|
|
Q_UNUSED(model);
|
|
if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonRelease &&
|
|
event->type() != QEvent::MouseButtonDblClick) return false;
|
|
auto* mouse = static_cast<QMouseEvent*>(event);
|
|
if (mouse->button() != Qt::LeftButton) return false;
|
|
const Action action = actionAt(mouse->pos(), option.rect, index.data(Qt::UserRole + 2).toBool());
|
|
const bool enabled = action == Action::Avatar || action == Action::RadioMute || index.data(MemberListModel::OnlineRole).toBool();
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
m_pressedIndex = action != Action::None && enabled ? index : QModelIndex();
|
|
m_pressedAction = action; m_pressedPosition = mouse->pos();
|
|
return action != Action::None;
|
|
}
|
|
if (event->type() == QEvent::MouseButtonDblClick) return action != Action::None;
|
|
const bool handled = action != Action::None || m_pressedIndex.isValid();
|
|
const bool activate = enabled && action != Action::None && action == m_pressedAction && m_pressedIndex == index &&
|
|
(mouse->pos() - m_pressedPosition).manhattanLength() < 5;
|
|
m_pressedIndex = QPersistentModelIndex(); m_pressedAction = Action::None;
|
|
if (activate) {
|
|
if (action == Action::Avatar) emit avatarRequested(index);
|
|
else if (action == Action::RadioMute) emit radioMuteRequested(index);
|
|
else if (action == Action::RadioCall) emit radioCallRequested(index);
|
|
else if (action == Action::Call) emit callRequested(index);
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
bool AccountDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option,
|
|
const QModelIndex& index) {
|
|
const bool self = index.data(Qt::UserRole + 2).toBool();
|
|
if (event && actionRect(option.rect, self ? Action::Call : Action::RadioMute).contains(event->pos())) {
|
|
const bool muted = index.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED;
|
|
const bool speaking = index.data(MemberListModel::RadioSpeakingRole).toBool();
|
|
QString text = QCoreApplication::translate("ChatGui", speaking ? "Radio: speaking" : "Radio: silent");
|
|
if (muted) text += " · " + QCoreApplication::translate("ChatGui", "Radio: muted");
|
|
if (!index.data(Qt::UserRole + 2).toBool()) text += '\n' + QCoreApplication::translate("ChatGui",
|
|
muted ? "Listen to this member in radio" : "Mute this member in radio");
|
|
QToolTip::showText(event->globalPos(), text, view);
|
|
return true;
|
|
}
|
|
if (event && !self) {
|
|
const Action action = actionAt(event->pos(), option.rect, false);
|
|
if (action == Action::RadioCall || action == Action::Call) {
|
|
QString text = QCoreApplication::translate("ChatGui", action == Action::RadioCall ? "Call by radio" : "Call peer");
|
|
if (action == Action::RadioCall && (index.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO))
|
|
text += QCoreApplication::translate("ChatGui", " · Radio enabled");
|
|
if (action == Action::Call && index.data(MemberListModel::OnlineRole).toBool()) {
|
|
const int flags = index.data(Qt::UserRole + 3).toInt();
|
|
text += " · " + QCoreApplication::translate("ChatGui", !(flags & CHAT_MEMBER_FLAG_PHONE_KNOWN) ? "Phone status unknown" :
|
|
(flags & CHAT_MEMBER_FLAG_PHONE_BUSY) ? "Busy on a call" : "Phone is free");
|
|
}
|
|
if (!index.data(MemberListModel::OnlineRole).toBool()) text += " · " + QCoreApplication::translate("ChatGui", "offline");
|
|
QToolTip::showText(event->globalPos(), text, view);
|
|
return true;
|
|
}
|
|
}
|
|
return QStyledItemDelegate::helpEvent(event, view, option, index);
|
|
}
|
|
|
|
QSize AccountDelegate::sizeHint(const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const {
|
|
(void)option; (void)index;
|
|
return QSize(160, MemberListModel::kRowHeight);
|
|
}
|