Skip to content

Commit

Permalink
fix: [notify] Rewrite notification item delegate with paint method
Browse files Browse the repository at this point in the history
Refactored notification item delegate to use direct painting instead of widget-based
approach for better performance and visual consistency. Key changes include:

- Removed widget-based item delegate implementation
- Added custom paint methods for background, icon, text and buttons
- Implemented mouse event handling for close button and action buttons
- Added text layout handling with proper wrapping and elision
- Introduced constants for layout metrics and spacing
- Added new roles and data structure for action button info

The new implementation provides smoother scrolling and consistent styling while
reducing memory usage by eliminating widget creation for each item.

Log: Refactor notification item delegate to use paint-based rendering
Bug: https://pms.uniontech.com/bug-view-272445.html
  • Loading branch information
Kakueeen authored and deepin-ci-robot committed Jan 3, 2025
1 parent 8fd2ff2 commit e7eda41
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 61 deletions.
15 changes: 15 additions & 0 deletions src/plugins/core/notify/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "notificationentity.h"

#include <QRect>

typedef std::shared_ptr<NotificationEntity> EntityPtr;

static const int ItemSpacing = 10; //消息通知内部Space
Expand All @@ -16,4 +18,17 @@ static const int BubbleEntities = 5;
static const char DefaultButtonField[] = "_default";
static const char NotificationContentName[] = "NotificationContent";

enum NotificationRole {
kActionsRole = Qt::UserRole + 1,
kSourceRole,
kEntityRole
};

struct ActionBtuuonInfo
{
QString id;
QString text;
QRect rect;
};

#endif // CONSTANTS_H
314 changes: 277 additions & 37 deletions src/plugins/core/notify/gui/itemdelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,317 @@

#include "itemdelegate.h"
#include "notificationlistview.h"
#include "notificationitemwidget.h"
#include "notify/constants.h"

#include <DStyle>
#include <DFrame>
#include <DFontSizeManager>
#include <DGuiApplicationHelper>
#include <DStyle>
#ifdef DTKWIDGET_CLASS_DPaletteHelper
# include <DPaletteHelper>
#endif

#include <QFontMetrics>
#include <QHBoxLayout>
#include <QApplication>
#include <QStyleOptionButton>

DWIDGET_USE_NAMESPACE

constexpr int kRadius { 8 };
constexpr int kNotificationSourceHeight { 15 };
constexpr int kNotificationLineWidth { 388 };
constexpr int kNotificationMessageWidth { 310 };
constexpr int kNotificationActionHeight { 32 };
constexpr int kNotificationSpacing { 10 };

ItemDelegate::ItemDelegate(NotificationListView *view, QObject *parent)
: QStyledItemDelegate(parent),
view(view)
{
}

QWidget *ItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
void ItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(option)
if (!index.isValid())
return QStyledItemDelegate::paint(painter, option, index);

EntityPtr notify = index.data().value<EntityPtr>();
if (!notify)
return nullptr;
QStyleOptionViewItem opt = option;
QStyledItemDelegate::initStyleOption(&opt, index);

DFrame *frame = new DFrame(parent);
frame->setBackgroundRole(DPalette::ItemBackground);
frame->setFrameRounded(true);

QHBoxLayout *layout = new QHBoxLayout(frame);
layout->setContentsMargins(10, 10, 10, 10);
painter->setRenderHint(QPainter::Antialiasing);
drawBackground(painter, opt);
auto iconRect = drawIcon(painter, opt, index);
drawDisplayText(painter, opt, index, iconRect);
drawActionButton(painter, option, index);
}

NotificationItemWidget *item = new NotificationItemWidget(parent, notify);
layout->addWidget(item);
bool ItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
if (event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonPress) {
Q_EMIT model->layoutChanged({ QPersistentModelIndex(index) });
return false;
} else if (event->type() == QEvent::MouseButtonRelease) {
const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
auto entity = index.data(kEntityRole).value<EntityPtr>();
QPoint mousePos = view->mapFromGlobal(QCursor::pos());
const auto &closeRect = closeButtonRect(option.rect);
if (closeRect.contains(mousePos)) {
Q_EMIT view->processed(entity);
return true;
}

connect(item, &NotificationItemWidget::actionInvoked, view, std::bind(&NotificationListView::actionInvoked, view, notify, std::placeholders::_1));
connect(item, &NotificationItemWidget::processed, view, &NotificationListView::processed);
const auto &actInfoList = actionInfoList(option, index);
for (const auto &info : actInfoList) {
if (info.rect.contains(mousePos)) {
Q_EMIT view->actionInvoked(entity, info.id);
Q_EMIT view->processed(entity);
return true;
}
}
}
}

return frame;
return false;
}

QSize ItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
EntityPtr notify = index.data().value<EntityPtr>();
Q_ASSERT(notify);

int height = 32;
QFontMetrics fontMetrics(qApp->font());
if (!notify->message().isEmpty()) {
QRect boundingRect = fontMetrics.boundingRect(0, 0, 300, 0, Qt::TextWordWrap, notify->message());
height += boundingRect.height() + 20;
}
int height = kNotificationSpacing * 2;
QString message = index.data(Qt::DisplayRole).toString();
auto f = DFontSizeManager::instance()->get(DFontSizeManager::T5);
QFontMetrics fontMetrics(f);
QRect boundingRect = fontMetrics.boundingRect(0, 0, kNotificationMessageWidth, 0, Qt::TextWordWrap, message);
height += boundingRect.height();

if (!notify->name().isEmpty()) {
QRect boundingRect = fontMetrics.boundingRect(0, 0, 300, 0, Qt::TextWordWrap, notify->name());
height += boundingRect.height() + 10;
QString source = index.data(kSourceRole).toString();
if (!source.isEmpty()) {
height += kNotificationSourceHeight + kNotificationSpacing;
}

if (!notify->actions().isEmpty()) {
height += 34;
auto actList = index.data(kActionsRole).toStringList();
if (!actList.isEmpty()) {
height += kNotificationActionHeight;
}

return { view->width(), height };
return { kNotificationLineWidth, height };
}

void ItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
void ItemDelegate::drawBackground(QPainter *painter, const QStyleOptionViewItem &option) const
{
Q_UNUSED(index)
painter->save();
#ifdef DTKWIDGET_CLASS_DPaletteHelper
DPalette palette = DPaletteHelper::instance()->palette(option.widget);
#else
DPalette palette = DGuiApplicationHelper::instance()->applicationPalette();
#endif
painter->setBrush(palette.brush(DPalette::ItemBackground));
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(option.rect, kRadius, kRadius);
painter->restore();
}

QRect ItemDelegate::drawIcon(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QRect rect = option.rect;
QSize size = sizeHint(option, index);
editor->setGeometry(rect.x(), rect.y(), size.width(), size.height() - ItemSpacing);
rect.setSize(view->iconSize());
rect.moveLeft(rect.left() + kNotificationSpacing);
rect.moveTop(rect.top() + kNotificationSpacing);

auto px = option.icon.pixmap(view->iconSize());
px.setDevicePixelRatio(qApp->devicePixelRatio());

painter->drawPixmap(rect, px);
return rect;
}

QRect ItemDelegate::drawDisplayText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &iconRect) const
{
QRect textRect = drawNotificationText(painter, option, index, iconRect);
textRect = drawSourceText(painter, option, index, textRect);
return textRect;
}

QRect ItemDelegate::drawNotificationText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &rect) const
{
QRect textRect = option.rect;
textRect.setLeft(rect.right() + kNotificationSpacing);
textRect.moveTop(textRect.top() + kNotificationSpacing - 4);
const auto &btnRect = drawCloseButton(painter, option);
textRect.setRight(btnRect.left() - kNotificationSpacing);

QTextOption textOption;
textOption.setWrapMode(QTextOption::WordWrap);
textOption.setTextDirection(option.direction);
textOption.setAlignment(QStyle::visualAlignment(option.direction, Qt::AlignLeft | Qt::AlignVCenter));

QTextLayout textLayout;
textLayout.setTextOption(textOption);
textLayout.setFont(option.font);
textLayout.setText(option.text);

QSizeF textLayoutSize = doTextLayout(&textLayout, textRect.width());
textRect.setSize(textLayoutSize.toSize());
textLayout.draw(painter, textRect.topLeft());
return textRect;
}

QRect ItemDelegate::drawSourceText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &rect) const
{
QString sourceText = index.data(kSourceRole).toString();
if (sourceText.isEmpty())
return rect;

QRect textRect = option.rect;
textRect.setLeft(rect.left());
textRect.moveTop(rect.bottom() + kNotificationSpacing - 4);

QTextOption textOption;
textOption.setWrapMode(QTextOption::NoWrap);
textOption.setTextDirection(option.direction);
textOption.setAlignment(QStyle::visualAlignment(option.direction, Qt::AlignLeft | Qt::AlignVCenter));

QTextLayout textLayout;
textLayout.setTextOption(textOption);
textLayout.setFont(DFontSizeManager::instance()->get(DFontSizeManager::T7, option.font));
textLayout.setText(sourceText);

QSizeF textLayoutSize = doTextLayout(&textLayout, textRect.width());
if (textRect.width() < textLayoutSize.width()) {
QString displayText = option.fontMetrics.elidedText(sourceText, Qt::ElideRight, textRect.width());
textLayout.setText(displayText);
textLayoutSize = doTextLayout(&textLayout, textRect.width());
}

#ifdef DTKWIDGET_CLASS_DPaletteHelper
DPalette palette = DPaletteHelper::instance()->palette(option.widget);
#else
DPalette palette = DGuiApplicationHelper::instance()->applicationPalette();
#endif
painter->save();
painter->setPen(palette.color(DPalette::TextTips));
textLayout.draw(painter, textRect.topLeft());
painter->restore();

return textRect;
}

QRect ItemDelegate::drawCloseButton(QPainter *painter, const QStyleOptionViewItem &option) const
{
const auto &rect = closeButtonRect(option.rect);
if (option.state & QStyle::State_MouseOver) {
DStyleOptionButton opt;
opt.icon = DStyle::standardIcon(option.widget->style(), DStyle::SP_CloseButton);
opt.iconSize = rect.size();
opt.rect = rect;
opt.state = QStyle::State_Enabled;
opt.features |= DStyleOptionButton::Flat;
opt.init(option.widget);

QPoint mousePos = view->mapFromGlobal(QCursor::pos());
bool isHovered = rect.contains(mousePos);
if (option.state & QStyle::State_MouseOver && isHovered) {
opt.state |= QStyle::State_MouseOver;
// pressed
if (QApplication::mouseButtons() & Qt::LeftButton)
opt.state |= QStyle::State_Sunken;
}

painter->save();
DStyle::drawControl(option.widget->style(), DStyle::CE_IconButton, &opt, painter, option.widget);
painter->restore();
}

return rect;
}

void ItemDelegate::drawActionButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
const auto &actInfoList = actionInfoList(option, index);
for (const auto &info : actInfoList) {
DStyleOptionButton opt;
opt.text = info.text;
opt.rect = info.rect;
opt.state = QStyle::State_Enabled;
opt.init(option.widget);

QPoint mousePos = view->mapFromGlobal(QCursor::pos());
bool isHovered = info.rect.contains(mousePos);
if (option.state & QStyle::State_MouseOver && isHovered) {
opt.state |= QStyle::State_MouseOver;
// pressed
if (QApplication::mouseButtons() & Qt::LeftButton)
opt.state |= QStyle::State_Sunken;
}

painter->save();
if (info.id.endsWith(DefaultButtonField)) {
opt.features |= QStyleOptionButton::ButtonFeature(DStyleOptionButton::SuggestButton);
QColor startColor = option.palette.color(QPalette::Highlight);
QColor endColor = DGuiApplicationHelper::adjustColor(startColor, 0, 0, +10, 0, 0, 0, 0);

opt.palette.setBrush(QPalette::Light, QBrush(endColor));
opt.palette.setBrush(QPalette::Dark, QBrush(startColor));
opt.palette.setBrush(QPalette::ButtonText, option.palette.highlightedText());
}

option.widget->style()->drawControl(QStyle::CE_PushButton, &opt, painter, option.widget);
painter->restore();
}
}

QSizeF ItemDelegate::doTextLayout(QTextLayout *textLayout, int width) const
{
qreal height = 0;
qreal widthUsed = 0;
textLayout->beginLayout();
while (true) {
QTextLine line = textLayout->createLine();
if (!line.isValid())
break;
line.setLineWidth(width);
line.setPosition(QPointF(0, height));
height += line.height();
widthUsed = qMax(widthUsed, line.naturalTextWidth());
}
textLayout->endLayout();
return QSizeF(widthUsed, height);
}

QList<ActionBtuuonInfo> ItemDelegate::actionInfoList(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
auto actList = index.data(kActionsRole).toStringList();
if (actList.isEmpty())
return {};

QList<ActionBtuuonInfo> actInfoList;
ActionBtuuonInfo info;
QRect tmpRect = option.rect;
for (int i = actList.size() - 1; i >= 0; --i) {
if (i % 2 == 0) {
info.id = actList[i];
actInfoList.append(info);
} else {
info.text = actList[i];
info.rect = tmpRect;
auto textRect = option.fontMetrics.boundingRect(0, 0, tmpRect.width(), 0, Qt::TextSingleLine, info.text);
textRect.adjust(-15, -5, 15, 5);
info.rect.setSize(textRect.size());
info.rect.moveRight(tmpRect.right() - kNotificationSpacing);
info.rect.moveBottom(option.rect.bottom() - kNotificationSpacing);
tmpRect.moveRight(info.rect.left());
}
}

return actInfoList;
}

QRect ItemDelegate::closeButtonRect(const QRect &itemRect) const
{
QRect rect = itemRect;
rect.setSize({ 24, 24 });
rect.moveRight(itemRect.right() - kNotificationSpacing);
rect.moveTop(itemRect.top() + kNotificationSpacing - 4);
return rect;
}
Loading

0 comments on commit e7eda41

Please sign in to comment.