From 61454ad660a5a4bcdb2656f3260ad457faf1d903 Mon Sep 17 00:00:00 2001 From: Liu Zhangjian Date: Thu, 2 Jan 2025 14:22:45 +0800 Subject: [PATCH] fix: [notify] Rewrite notification item delegate with paint method 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 --- src/plugins/core/notify/constants.h | 15 + src/plugins/core/notify/gui/itemdelegate.cpp | 314 +++++++++++++++--- src/plugins/core/notify/gui/itemdelegate.h | 19 +- .../notify/gui/notificationcenterwidget.cpp | 6 +- .../core/notify/gui/notificationlistview.cpp | 9 +- .../core/notify/gui/notificationmodel.cpp | 40 ++- .../core/notify/gui/notificationmodel.h | 1 - 7 files changed, 343 insertions(+), 61 deletions(-) diff --git a/src/plugins/core/notify/constants.h b/src/plugins/core/notify/constants.h index e27396895..d6ba1d21f 100644 --- a/src/plugins/core/notify/constants.h +++ b/src/plugins/core/notify/constants.h @@ -7,6 +7,8 @@ #include "notificationentity.h" +#include + typedef std::shared_ptr EntityPtr; static const int ItemSpacing = 10; //消息通知内部Space @@ -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 diff --git a/src/plugins/core/notify/gui/itemdelegate.cpp b/src/plugins/core/notify/gui/itemdelegate.cpp index 757e5861f..3dcf7aa1e 100644 --- a/src/plugins/core/notify/gui/itemdelegate.cpp +++ b/src/plugins/core/notify/gui/itemdelegate.cpp @@ -4,77 +4,317 @@ #include "itemdelegate.h" #include "notificationlistview.h" -#include "notificationitemwidget.h" -#include "notify/constants.h" #include #include +#include +#include +#include +#ifdef DTKWIDGET_CLASS_DPaletteHelper +# include +#endif #include #include #include +#include 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(); - 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(event); + if (mouseEvent->button() == Qt::LeftButton) { + auto entity = index.data(kEntityRole).value(); + 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(); - 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 ItemDelegate::actionInfoList(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + auto actList = index.data(kActionsRole).toStringList(); + if (actList.isEmpty()) + return {}; + + QList 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; } diff --git a/src/plugins/core/notify/gui/itemdelegate.h b/src/plugins/core/notify/gui/itemdelegate.h index 7234ba18d..fcff9082f 100644 --- a/src/plugins/core/notify/gui/itemdelegate.h +++ b/src/plugins/core/notify/gui/itemdelegate.h @@ -5,7 +5,10 @@ #ifndef ITEMDELEGATE_H #define ITEMDELEGATE_H +#include "notify/constants.h" + #include +#include class NotificationListView; class ItemDelegate : public QStyledItemDelegate @@ -13,11 +16,23 @@ class ItemDelegate : public QStyledItemDelegate public: explicit ItemDelegate(NotificationListView *view, QObject *parent = nullptr); - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: + void drawBackground(QPainter *painter, const QStyleOptionViewItem &option) const; + QRect drawIcon(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + QRect drawDisplayText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &iconRect) const; + QRect drawNotificationText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &rect) const; + QRect drawSourceText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QRect &rect) const; + QRect drawCloseButton(QPainter *painter, const QStyleOptionViewItem &option) const; + void drawActionButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + + QSizeF doTextLayout(QTextLayout *textLayout, int width) const; + QList actionInfoList(const QStyleOptionViewItem &option, const QModelIndex &index) const; + QRect closeButtonRect(const QRect &itemRect) const; + NotificationListView *view { nullptr }; }; diff --git a/src/plugins/core/notify/gui/notificationcenterwidget.cpp b/src/plugins/core/notify/gui/notificationcenterwidget.cpp index f1685d471..0e1524192 100644 --- a/src/plugins/core/notify/gui/notificationcenterwidget.cpp +++ b/src/plugins/core/notify/gui/notificationcenterwidget.cpp @@ -48,9 +48,11 @@ void NotificationCenterWidgetPrivate::initUI() q->setBackgroundRole(QPalette::Base); QVBoxLayout *mainLayout = new QVBoxLayout(q); - mainLayout->setContentsMargins(10, 10, 10, 10); + mainLayout->setContentsMargins(5, 10, 5, 10); + mainLayout->setSpacing(5); QLabel *titleLabel = new QLabel(NotificationCenterWidget::tr("Notification"), q); + DFontSizeManager::instance()->bind(titleLabel, DFontSizeManager::T5); hideBtn = new DToolButton(q); hideBtn->setIconSize({ 16, 16 }); hideBtn->setIcon(QIcon::fromTheme("hide")); @@ -60,7 +62,7 @@ void NotificationCenterWidgetPrivate::initUI() clearBtn->setIcon(QIcon::fromTheme("clear_history")); QHBoxLayout *titleLayout = new QHBoxLayout; - titleLayout->setContentsMargins(10, 0, 10, 0); + titleLayout->setContentsMargins(15, 0, 15, 0); titleLayout->addWidget(titleLabel); titleLayout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Expanding)); titleLayout->addWidget(hideBtn); diff --git a/src/plugins/core/notify/gui/notificationlistview.cpp b/src/plugins/core/notify/gui/notificationlistview.cpp index 3b91150e6..b3d8dd0fc 100644 --- a/src/plugins/core/notify/gui/notificationlistview.cpp +++ b/src/plugins/core/notify/gui/notificationlistview.cpp @@ -11,15 +11,10 @@ DWIDGET_USE_NAMESPACE NotificationListView::NotificationListView(QWidget *parent) : DListView(parent) { - setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - QScroller::grabGesture(this, QScroller::LeftMouseButtonGesture); - QScroller *scroller = QScroller::scroller(this); - QScrollerProperties sp; - sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); - scroller->setScrollerProperties(sp); + setIconSize({ 16, 16 }); + setSpacing(5); } NotificationListView::~NotificationListView() { - } diff --git a/src/plugins/core/notify/gui/notificationmodel.cpp b/src/plugins/core/notify/gui/notificationmodel.cpp index 33595ad34..0cc0d7822 100644 --- a/src/plugins/core/notify/gui/notificationmodel.cpp +++ b/src/plugins/core/notify/gui/notificationmodel.cpp @@ -53,22 +53,38 @@ QVariant NotificationModel::data(const QModelIndex &index, int role) const } EntityPtr entity = d->getEntityByRow(index.row()); - if (entity) - return QVariant::fromValue(entity); + if (entity) { + switch (role) { + case Qt::DecorationRole: { + auto type = entity->type(); + switch (type) { + case NotificationEntity::Information: + return QIcon::fromTheme("notification_info"); + case NotificationEntity::Warning: + return QIcon::fromTheme("notification_warning"); + case NotificationEntity::Error: + return QIcon::fromTheme("notification_error"); + } + } + case Qt::DisplayRole: + return entity->message(); + case kActionsRole: + return entity->actions(); + case kSourceRole: { + QString source = entity->name(); + if (!source.isEmpty()) + return tr("Source: %1").arg(source); + } + case kEntityRole: + return qVariantFromValue(entity); + default: + break; + } + } return QVariant(); } -Qt::ItemFlags NotificationModel::flags(const QModelIndex &index) const -{ - if (index.isValid()) { - if (d->view) - d->view->openPersistentEditor(index); - return QAbstractListModel::flags(index) | Qt::ItemIsEditable; - } - return QAbstractListModel::flags(index); -} - void NotificationModel::setNotifications(const QList &datas) { beginResetModel(); diff --git a/src/plugins/core/notify/gui/notificationmodel.h b/src/plugins/core/notify/gui/notificationmodel.h index a81c6a93c..7bbc03851 100644 --- a/src/plugins/core/notify/gui/notificationmodel.h +++ b/src/plugins/core/notify/gui/notificationmodel.h @@ -20,7 +20,6 @@ class NotificationModel : public QAbstractListModel int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; public Q_SLOTS: void setNotifications(const QList &datas);