feat: responsive compact mail view + folder unread badges
- CompactMailDelegate: card-based view for narrow panels (<420px) * Avatar with hash-based color, sender + date (relative format) * Subject line with attachment icon * Action buttons: flag, delete, category, more (context menu) * Unread highlight with light blue background + bold sender - MailListView: automatic switch between tree/table (wide) and list/cards (narrow) * QStackedWidget with QTreeView + QListView sharing same proxy model * resizeEvent threshold at 420px * Signals forwarded to MainMainWindow for actions - FolderTreeDelegate: custom paint for folder tree * Unread count badge (blue pill) right-aligned on folder nodes * Account nodes bold, folder nodes normal weight * Proper indentation per depth level - MailItemDao::countUnreadByFolderId() for real-time unread counts - MainMainWindow handlers for compact actions (flag, delete, category, more menu)
This commit is contained in:
@@ -394,3 +394,19 @@ QVector<StoredAttachmentRecord> MailItemDao::attachmentsForMail(qint64 mailItemI
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int MailItemDao::countUnreadByFolderId(int folderId)
|
||||
{
|
||||
QSqlDatabase db = DatabaseManager::instance().database();
|
||||
QSqlQuery query(db);
|
||||
query.prepare("SELECT COUNT(*) FROM MailCopy WHERE folderId = :folderId AND read = 0");
|
||||
query.bindValue(":folderId", folderId);
|
||||
if (!query.exec()) {
|
||||
qWarning() << "Failed to count unread mails:" << query.lastError().text();
|
||||
return 0;
|
||||
}
|
||||
if (query.next()) {
|
||||
return query.value(0).toInt();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -32,4 +32,5 @@ public:
|
||||
static bool removeByUid(int folderId, qint64 uid);
|
||||
static bool replaceAttachments(qint64 mailItemId, const QVector<StoredAttachmentRecord>& attachments);
|
||||
static QVector<StoredAttachmentRecord> attachmentsForMail(qint64 mailItemId);
|
||||
static int countUnreadByFolderId(int folderId);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
#include "CompactMailDelegate.h"
|
||||
#include <QDebug>
|
||||
|
||||
CompactMailDelegate::CompactMailDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void CompactMailDelegate::setConfig(const Config& config)
|
||||
{
|
||||
m_config = config;
|
||||
}
|
||||
|
||||
QSize CompactMailDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(option);
|
||||
Q_UNUSED(index);
|
||||
return QSize(0, m_config.cardHeight); // width will be determined by view
|
||||
}
|
||||
|
||||
void CompactMailDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||
|
||||
drawCard(painter, option, index);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
bool CompactMailDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonRelease) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
|
||||
QPoint pos = mouseEvent->pos();
|
||||
|
||||
// Check if click is on an action button
|
||||
for (const ActionButton &btn : m_lastButtons) {
|
||||
if (btn.rect.contains(pos)) {
|
||||
int mailId = index.data(EmailListModel::IdRole).toInt();
|
||||
switch (btn.type) {
|
||||
case ActionFlag:
|
||||
emit flagRequested(mailId);
|
||||
break;
|
||||
case ActionDelete:
|
||||
emit deleteRequested(mailId);
|
||||
break;
|
||||
case ActionCategory:
|
||||
emit categoryRequested(mailId);
|
||||
break;
|
||||
case ActionMore:
|
||||
emit moreRequested(mailId, mouseEvent->globalPosition().toPoint());
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Click on card body (not buttons) - select mail
|
||||
int mailId = index.data(EmailListModel::IdRole).toInt();
|
||||
emit mailClicked(mailId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::editorEvent(event, model, option, index);
|
||||
}
|
||||
|
||||
void CompactMailDelegate::drawCard(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
QRect cardRect = option.rect.adjusted(4, 2, -4, -2); // margins
|
||||
bool isSelected = option.state & QStyle::State_Selected;
|
||||
bool isHovered = option.state & QStyle::State_MouseOver;
|
||||
bool isUnread = !index.data(EmailListModel::ReadRole).toBool();
|
||||
|
||||
// Background
|
||||
QColor bgColor;
|
||||
if (isSelected) {
|
||||
bgColor = m_config.selectedBgColor;
|
||||
} else if (isHovered) {
|
||||
bgColor = m_config.hoverBgColor;
|
||||
} else if (isUnread) {
|
||||
bgColor = m_config.unreadBgColor;
|
||||
} else {
|
||||
bgColor = m_config.readBgColor;
|
||||
}
|
||||
|
||||
painter->setBrush(bgColor);
|
||||
painter->setPen(QColor(0xe8, 0xe8, 0xed)); // separator color
|
||||
painter->drawRoundedRect(cardRect, 8, 8);
|
||||
|
||||
// Inner padding
|
||||
QRect contentRect = cardRect.adjusted(12, 8, -12, -8);
|
||||
|
||||
// Data from model
|
||||
QString sender = index.data(EmailListModel::SenderRole).toString();
|
||||
QString subject = index.data(EmailListModel::SubjectRole).toString();
|
||||
QDateTime date = index.data(EmailListModel::DateRole).toDateTime();
|
||||
bool hasAttachments = !index.data(EmailListModel::AttachmentsRole).value<QVector<QString>>().isEmpty();
|
||||
bool flagged = index.data(EmailListModel::FlaggedRole).toBool();
|
||||
|
||||
int x = contentRect.left();
|
||||
int y = contentRect.top();
|
||||
int availableWidth = contentRect.width();
|
||||
|
||||
// Avatar
|
||||
if (m_config.showAvatar) {
|
||||
QRect avatarRect(x, y, m_config.avatarSize, m_config.avatarSize);
|
||||
avatarRect.moveTop(y + (contentRect.height() - m_config.avatarSize) / 2);
|
||||
drawAvatar(painter, avatarRect, sender);
|
||||
x += m_config.avatarSize + 10;
|
||||
availableWidth -= m_config.avatarSize + 10;
|
||||
}
|
||||
|
||||
// Fonts
|
||||
QFont senderFont = QApplication::font();
|
||||
senderFont.setPointSizeF(13);
|
||||
senderFont.setWeight(isUnread ? QFont::DemiBold : QFont::Medium);
|
||||
|
||||
QFont subjectFont = QApplication::font();
|
||||
subjectFont.setPointSizeF(12);
|
||||
subjectFont.setWeight(QFont::Normal);
|
||||
|
||||
QFont dateFont = QApplication::font();
|
||||
dateFont.setPointSizeF(11);
|
||||
|
||||
QFont actionFont = QApplication::font();
|
||||
actionFont.setPointSizeF(11);
|
||||
|
||||
// Line 1: Sender (left) + Date (right)
|
||||
int line1Y = y + 2;
|
||||
QString dateStr = m_config.showDateRelative ? formatDateRelative(date) : date.toString("dd/MM/yyyy HH:mm");
|
||||
|
||||
QFontMetrics senderFM(senderFont);
|
||||
QFontMetrics dateFM(dateFont);
|
||||
|
||||
int dateWidth = dateFM.horizontalAdvance(dateStr);
|
||||
int senderMaxWidth = availableWidth - dateWidth - 8;
|
||||
|
||||
QString elidedSender = elideText(painter, sender, senderMaxWidth, senderFont);
|
||||
|
||||
// Draw sender
|
||||
painter->setFont(senderFont);
|
||||
painter->setPen(isUnread ? m_config.textPrimaryColor : m_config.textPrimaryColor);
|
||||
painter->drawText(QRect(x, line1Y, senderMaxWidth, senderFM.height()), Qt::AlignLeft | Qt::AlignVCenter, elidedSender);
|
||||
|
||||
// Draw date
|
||||
painter->setFont(dateFont);
|
||||
painter->setPen(m_config.textSecondaryColor);
|
||||
painter->drawText(QRect(x + senderMaxWidth + 8, line1Y, dateWidth, dateFM.height()), Qt::AlignRight | Qt::AlignVCenter, dateStr);
|
||||
|
||||
// Line 2: Attachment icon + Subject
|
||||
int line2Y = line1Y + senderFM.height() + 2;
|
||||
QString subjectPrefix = "";
|
||||
int prefixWidth = 0;
|
||||
|
||||
if (m_config.showAttachmentIcon && hasAttachments) {
|
||||
subjectPrefix = "📎 ";
|
||||
painter->setFont(subjectFont);
|
||||
prefixWidth = painter->fontMetrics().horizontalAdvance(subjectPrefix);
|
||||
}
|
||||
|
||||
int subjectMaxWidth = availableWidth - prefixWidth;
|
||||
QString elidedSubject = elideText(painter, subject, subjectMaxWidth, subjectFont);
|
||||
|
||||
painter->setFont(subjectFont);
|
||||
painter->setPen(isUnread ? m_config.textPrimaryColor : m_config.textSecondaryColor);
|
||||
painter->drawText(QRect(x, line2Y, prefixWidth, subjectFont.pointSizeF()), Qt::AlignLeft | Qt::AlignVCenter, subjectPrefix);
|
||||
painter->drawText(QRect(x + prefixWidth, line2Y, subjectMaxWidth, subjectFont.pointSizeF()), Qt::AlignLeft | Qt::AlignVCenter, elidedSubject);
|
||||
|
||||
// Action buttons (bottom)
|
||||
int buttonAreaY = cardRect.bottom() - 28;
|
||||
QRect buttonAreaRect(contentRect.left(), buttonAreaY, contentRect.width(), 24);
|
||||
|
||||
m_lastButtons = layoutActionButtons(buttonAreaRect);
|
||||
m_lastIndex = index;
|
||||
|
||||
drawActionButtons(painter, buttonAreaRect, index, m_lastButtons);
|
||||
|
||||
// Flag indicator (small dot on top-right of avatar area if flagged)
|
||||
if (flagged && m_config.showAvatar) {
|
||||
QRect flagRect(x - m_config.avatarSize - 10 + m_config.avatarSize - 8, y, 12, 12);
|
||||
painter->setBrush(m_config.flagColor);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->drawEllipse(flagRect);
|
||||
}
|
||||
}
|
||||
|
||||
void CompactMailDelegate::drawAvatar(QPainter *painter, const QRect &rect, const QString &sender) const
|
||||
{
|
||||
QColor bgColor = avatarColorForSender(sender);
|
||||
painter->setBrush(bgColor);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->drawEllipse(rect);
|
||||
|
||||
// Initial
|
||||
QString initial;
|
||||
if (!sender.isEmpty()) {
|
||||
// Extract first letter of first name or email
|
||||
QString clean = sender;
|
||||
int lt = clean.indexOf('<');
|
||||
if (lt > 0) clean = clean.left(lt).trimmed();
|
||||
QStringList parts = clean.split(' ', Qt::SkipEmptyParts);
|
||||
if (!parts.isEmpty()) {
|
||||
initial = parts[0].at(0).toUpper();
|
||||
} else if (!clean.isEmpty()) {
|
||||
initial = clean.at(0).toUpper();
|
||||
}
|
||||
}
|
||||
if (initial.isEmpty()) initial = "?";
|
||||
|
||||
QFont font = QApplication::font();
|
||||
font.setPointSizeF(m_config.avatarSize * 0.4);
|
||||
font.setWeight(QFont::Bold);
|
||||
painter->setFont(font);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(rect, Qt::AlignCenter, initial);
|
||||
}
|
||||
|
||||
QColor CompactMailDelegate::avatarColorForSender(const QString &sender) const
|
||||
{
|
||||
// Hash-based color from sender email/name
|
||||
uint hash = 0;
|
||||
for (QChar c : sender) {
|
||||
hash = hash * 31 + c.unicode();
|
||||
}
|
||||
|
||||
// Predefined pleasant colors (macOS Mail style)
|
||||
static const QColor colors[] = {
|
||||
QColor(0xff, 0x3b, 0x30), // red
|
||||
QColor(0xff, 0x95, 0x00), // orange
|
||||
QColor(0xff, 0xcc, 0x00), // yellow
|
||||
QColor(0x34, 0xc7, 0x59), // green
|
||||
QColor(0x00, 0x71, 0xe3), // blue
|
||||
QColor(0x5e, 0x5c, 0xe6), // purple
|
||||
QColor(0xff, 0x2d, 0x92), // pink
|
||||
QColor(0x00, 0xc7, 0xbe), // teal
|
||||
QColor(0xaf, 0x52, 0xde), // indigo
|
||||
QColor(0xbf, 0x5a, 0xf2), // violet
|
||||
};
|
||||
|
||||
return colors[hash % 10];
|
||||
}
|
||||
|
||||
CompactMailDelegate::ActionButton CompactMailDelegate::makeButton(ActionButtonType type, const QRect &rect) const
|
||||
{
|
||||
QString icon, tooltip;
|
||||
switch (type) {
|
||||
case ActionFlag:
|
||||
icon = "★";
|
||||
tooltip = "Marcar para seguimiento";
|
||||
break;
|
||||
case ActionDelete:
|
||||
icon = "🗑";
|
||||
tooltip = "Borrar";
|
||||
break;
|
||||
case ActionCategory:
|
||||
icon = "🏷";
|
||||
tooltip = "Categoría";
|
||||
break;
|
||||
case ActionMore:
|
||||
icon = "⋯";
|
||||
tooltip = "Más opciones";
|
||||
break;
|
||||
}
|
||||
return {type, icon, tooltip, rect};
|
||||
}
|
||||
|
||||
QVector<CompactMailDelegate::ActionButton> CompactMailDelegate::layoutActionButtons(const QRect &areaRect) const
|
||||
{
|
||||
QVector<ActionButton> buttons;
|
||||
int buttonSize = 24;
|
||||
int spacing = 8;
|
||||
int totalWidth = m_config.actionOrder.size() * buttonSize + (m_config.actionOrder.size() - 1) * spacing;
|
||||
int startX = areaRect.left() + (areaRect.width() - totalWidth) / 2;
|
||||
int y = areaRect.top() + (areaRect.height() - buttonSize) / 2;
|
||||
|
||||
for (int i = 0; i < m_config.actionOrder.size(); ++i) {
|
||||
ActionButtonType type = m_config.actionOrder[i];
|
||||
QRect btnRect(startX + i * (buttonSize + spacing), y, buttonSize, buttonSize);
|
||||
buttons.append(makeButton(type, btnRect));
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
void CompactMailDelegate::drawActionButtons(QPainter *painter, const QRect &areaRect, const QModelIndex &index, const QVector<ActionButton> &buttons) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
Q_UNUSED(areaRect);
|
||||
|
||||
QFont font = QApplication::font();
|
||||
font.setPointSizeF(13);
|
||||
painter->setFont(font);
|
||||
|
||||
for (const ActionButton &btn : buttons) {
|
||||
// Icon
|
||||
painter->setPen(m_config.textSecondaryColor);
|
||||
painter->drawText(btn.rect, Qt::AlignCenter, btn.icon);
|
||||
}
|
||||
}
|
||||
|
||||
QString CompactMailDelegate::formatDateRelative(const QDateTime &date) const
|
||||
{
|
||||
if (!date.isValid()) return "";
|
||||
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
if (date.timeSpec() != Qt::UTC) {
|
||||
date.toUTC();
|
||||
}
|
||||
|
||||
int secs = date.secsTo(now);
|
||||
if (secs < 0) secs = 0;
|
||||
|
||||
if (secs < 60) return "ahora";
|
||||
if (secs < 3600) return QString("%1m").arg(secs / 60);
|
||||
if (secs < 86400) return QString("%1h").arg(secs / 3600);
|
||||
|
||||
QDate d = date.date();
|
||||
QDate today = QDate::currentDate();
|
||||
if (d == today) return date.toString("HH:mm");
|
||||
if (d == today.addDays(-1)) return "Ayer";
|
||||
if (d > today.addDays(-7)) return d.toString("ddd"); // "lun", "mar"...
|
||||
if (d.year() == today.year()) return d.toString("dd/MM");
|
||||
return d.toString("dd/MM/yy");
|
||||
}
|
||||
|
||||
QString CompactMailDelegate::elideText(QPainter *painter, const QString &text, int maxWidth, const QFont &font) const
|
||||
{
|
||||
if (maxWidth <= 0) return "";
|
||||
QFontMetrics fm(font);
|
||||
return fm.elidedText(text, Qt::ElideRight, maxWidth);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionViewItem>
|
||||
#include <QModelIndex>
|
||||
#include <QEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QApplication>
|
||||
#include <QStyle>
|
||||
#include <QPixmap>
|
||||
#include <QFontMetrics>
|
||||
#include <QColor>
|
||||
#include <QRect>
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
#include "ui/models/EmailListModel.h"
|
||||
|
||||
class CompactMailDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ActionButtonType {
|
||||
ActionFlag = 0,
|
||||
ActionDelete = 1,
|
||||
ActionCategory = 2,
|
||||
ActionMore = 3
|
||||
};
|
||||
|
||||
struct ActionButton {
|
||||
ActionButtonType type;
|
||||
QString icon;
|
||||
QString tooltip;
|
||||
QRect rect;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
bool showAvatar = true;
|
||||
int avatarSize = 36;
|
||||
int cardHeight = 76;
|
||||
int thresholdWidth = 420;
|
||||
QVector<ActionButtonType> actionOrder = {ActionFlag, ActionDelete, ActionCategory, ActionMore};
|
||||
bool showAttachmentIcon = true;
|
||||
bool showDateRelative = true;
|
||||
QColor unreadBgColor = QColor(0xe3, 0xf2, 0xfd); // light blue
|
||||
QColor readBgColor = QColor(0xff, 0xff, 0xff);
|
||||
QColor hoverBgColor = QColor(0xf0, 0xf0, 0xf5);
|
||||
QColor selectedBgColor = QColor(0xd6, 0xe8, 0xf7);
|
||||
QColor textPrimaryColor = QColor(0x1d, 0x1d, 0x1f);
|
||||
QColor textSecondaryColor = QColor(0x86, 0x86, 0x8b);
|
||||
QColor accentColor = QColor(0x00, 0x71, 0xe3);
|
||||
QColor flagColor = QColor(0xff, 0x3b, 0x30);
|
||||
};
|
||||
|
||||
explicit CompactMailDelegate(QObject *parent = nullptr);
|
||||
~CompactMailDelegate() override = default;
|
||||
|
||||
void setConfig(const Config& config);
|
||||
Config config() const { return m_config; }
|
||||
|
||||
// QStyledItemDelegate interface
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override;
|
||||
|
||||
signals:
|
||||
void flagRequested(int mailId);
|
||||
void deleteRequested(int mailId);
|
||||
void categoryRequested(int mailId);
|
||||
void moreRequested(int mailId, const QPoint& globalPos); // for context menu
|
||||
void mailClicked(int mailId);
|
||||
|
||||
private:
|
||||
void drawCard(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
void drawAvatar(QPainter *painter, const QRect &rect, const QString &sender) const;
|
||||
QColor avatarColorForSender(const QString &sender) const;
|
||||
void drawActionButtons(QPainter *painter, const QRect &cardRect, const QModelIndex &index, const QVector<ActionButton>& buttons) const;
|
||||
QVector<ActionButton> layoutActionButtons(const QRect &cardRect) const;
|
||||
ActionButton makeButton(ActionButtonType type, const QRect &rect) const;
|
||||
QString formatDateRelative(const QDateTime &date) const;
|
||||
QString elideText(QPainter *painter, const QString &text, int maxWidth, const QFont &font) const;
|
||||
|
||||
Config m_config;
|
||||
mutable QVector<ActionButton> m_lastButtons; // for hit testing
|
||||
mutable QModelIndex m_lastIndex;
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "FolderTreeDelegate.h"
|
||||
#include <QPainter>
|
||||
#include <QFontMetrics>
|
||||
#include <QApplication>
|
||||
#include "ui/models/FolderListModel.h"
|
||||
|
||||
FolderTreeDelegate::FolderTreeDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void FolderTreeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||
|
||||
QRect rect = option.rect;
|
||||
bool isSelected = option.state & QStyle::State_Selected;
|
||||
bool isHovered = option.state & QStyle::State_MouseOver;
|
||||
|
||||
// Get data
|
||||
QString name = index.data(FolderListModel::NameRole).toString();
|
||||
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
|
||||
int unreadCount = index.data(FolderListModel::UnreadCountRole).toInt();
|
||||
|
||||
// Background
|
||||
if (isSelected) {
|
||||
painter->fillRect(rect, m_selectedBgColor);
|
||||
} else if (isHovered) {
|
||||
painter->fillRect(rect, m_hoverBgColor);
|
||||
}
|
||||
|
||||
// Indentation for folder nodes
|
||||
// FolderTreeItem::AccountNode = 0, FolderTreeItem::FolderNode = 1
|
||||
int indent = 0;
|
||||
if (itemType == 1) { // FolderNode
|
||||
// Get depth from parent hierarchy
|
||||
QModelIndex parent = index.parent();
|
||||
int depth = 0;
|
||||
while (parent.isValid()) {
|
||||
depth++;
|
||||
parent = parent.parent();
|
||||
}
|
||||
indent = depth * 16; // 16px per level
|
||||
}
|
||||
|
||||
QRect textRect = rect.adjusted(12 + indent, 0, -40, 0); // leave 40px for unread count
|
||||
|
||||
// Font
|
||||
QFont font = QApplication::font();
|
||||
if (itemType == 0) { // AccountNode
|
||||
font.setWeight(QFont::DemiBold);
|
||||
font.setPointSizeF(12);
|
||||
} else {
|
||||
font.setWeight(QFont::Normal);
|
||||
font.setPointSizeF(12);
|
||||
}
|
||||
painter->setFont(font);
|
||||
|
||||
// Text color
|
||||
if (isSelected) {
|
||||
painter->setPen(m_unreadTextColor); // white on blue selection
|
||||
} else {
|
||||
painter->setPen(m_textPrimaryColor);
|
||||
}
|
||||
|
||||
// Draw folder/account name
|
||||
QFontMetrics fm(font);
|
||||
QString elidedName = fm.elidedText(name, Qt::ElideRight, textRect.width());
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, elidedName);
|
||||
|
||||
// Draw unread count badge (right-aligned)
|
||||
if (unreadCount > 0 && itemType == 1) { // FolderNode
|
||||
QString countStr = unreadCount > 99 ? "99+" : QString::number(unreadCount);
|
||||
QFont badgeFont = font;
|
||||
badgeFont.setPointSizeF(10);
|
||||
badgeFont.setWeight(QFont::Medium);
|
||||
painter->setFont(badgeFont);
|
||||
|
||||
QFontMetrics badgeFm(badgeFont);
|
||||
int badgeWidth = badgeFm.horizontalAdvance(countStr) + 12;
|
||||
int badgeHeight = 18;
|
||||
|
||||
QRect badgeRect(rect.right() - 32 - badgeWidth, rect.center().y() - badgeHeight / 2, badgeWidth, badgeHeight);
|
||||
|
||||
// Badge background
|
||||
painter->setBrush(m_unreadBgColor);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->drawRoundedRect(badgeRect, badgeHeight / 2, badgeHeight / 2);
|
||||
|
||||
// Badge text
|
||||
painter->setPen(m_unreadTextColor);
|
||||
painter->drawText(badgeRect, Qt::AlignCenter, countStr);
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize FolderTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
QSize size = QStyledItemDelegate::sizeHint(option, index);
|
||||
return QSize(size.width(), 28); // Fixed height for folder items
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionViewItem>
|
||||
#include <QModelIndex>
|
||||
#include "ui/models/FolderListModel.h"
|
||||
|
||||
class FolderTreeItem; // forward declaration
|
||||
|
||||
class FolderTreeDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FolderTreeDelegate(QObject *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
QColor m_unreadBgColor = QColor(0x00, 0x71, 0xe3); // blue
|
||||
QColor m_unreadTextColor = Qt::white;
|
||||
QColor m_textPrimaryColor = QColor(0x1d, 0x1d, 0x1f);
|
||||
QColor m_textSecondaryColor = QColor(0x86, 0x86, 0x8b);
|
||||
QColor m_selectedBgColor = QColor(0xe3, 0xf2, 0xfd);
|
||||
QColor m_hoverBgColor = QColor(0xf0, 0xf0, 0xf5);
|
||||
};
|
||||
+125
-25
@@ -11,12 +11,18 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QHeaderView>
|
||||
#include <QResizeEvent>
|
||||
#include <QListView>
|
||||
#include <QStyledItemDelegate>
|
||||
#include "ui/delegates/CompactMailDelegate.h"
|
||||
|
||||
MailListView::MailListView(QWidget *parent) : QWidget(parent), m_sourceModel(nullptr) {
|
||||
MailListView::MailListView(QWidget *parent) : QWidget(parent), m_sourceModel(nullptr)
|
||||
{
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void MailListView::setupUI() {
|
||||
void MailListView::setupUI()
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
@@ -81,14 +87,18 @@ void MailListView::setupUI() {
|
||||
|
||||
layout->addWidget(filterBar);
|
||||
|
||||
// Tree View
|
||||
// View Stack (TreeView for normal, ListView for compact)
|
||||
m_viewStack = new QStackedWidget();
|
||||
layout->addWidget(m_viewStack, 1);
|
||||
|
||||
// --- Normal Tree View ---
|
||||
m_treeView = new QTreeView();
|
||||
m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_treeView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_treeView->setAlternatingRowColors(true);
|
||||
m_treeView->header()->setStretchLastSection(false);
|
||||
m_treeView->header()->setSectionsClickable(true);
|
||||
m_treeView->setSortingEnabled(false); // We handle sorting in the model
|
||||
m_treeView->setSortingEnabled(false);
|
||||
m_treeView->setFrameShape(QFrame::NoFrame);
|
||||
m_treeView->setRootIsDecorated(true);
|
||||
m_treeView->setItemsExpandable(true);
|
||||
@@ -101,16 +111,14 @@ void MailListView::setupUI() {
|
||||
"QHeaderView::section { background-color: #f5f5f7; padding: 8px; border: none; border-bottom: 1px solid #d1d1d6; font-weight: 600; color: #555; }"
|
||||
);
|
||||
|
||||
// Tree model
|
||||
m_treeModel = new EmailTreeModel(this);
|
||||
m_proxyModel = new QSortFilterProxyModel(this);
|
||||
m_proxyModel->setSourceModel(m_treeModel);
|
||||
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_proxyModel->setFilterKeyColumn(-1); // Search all columns
|
||||
m_proxyModel->setFilterKeyColumn(-1);
|
||||
|
||||
m_treeView->setModel(m_proxyModel);
|
||||
|
||||
// Column widths
|
||||
m_treeView->header()->setSectionResizeMode(EmailTreeModel::ColSubject, QHeaderView::Stretch);
|
||||
m_treeView->header()->setSectionResizeMode(EmailTreeModel::ColSender, QHeaderView::Stretch);
|
||||
m_treeView->header()->setSectionResizeMode(EmailTreeModel::ColDate, QHeaderView::ResizeToContents);
|
||||
@@ -125,10 +133,46 @@ void MailListView::setupUI() {
|
||||
}
|
||||
});
|
||||
|
||||
// Expand all groups by default
|
||||
m_treeView->expandAll();
|
||||
|
||||
layout->addWidget(m_treeView);
|
||||
// --- Compact List View ---
|
||||
m_listView = new QListView();
|
||||
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_listView->setUniformItemSizes(true);
|
||||
m_listView->setFrameShape(QFrame::NoFrame);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setStyleSheet(
|
||||
"QListView { background-color: #ffffff; border: none; outline: none; }"
|
||||
"QListView::item { border: none; }"
|
||||
"QListView::item:selected { background: transparent; }"
|
||||
"QListView::item:hover { background: transparent; }"
|
||||
);
|
||||
|
||||
m_compactDelegate = new CompactMailDelegate(this);
|
||||
m_listView->setItemDelegate(m_compactDelegate);
|
||||
|
||||
// Connect compact delegate signals
|
||||
connect(m_compactDelegate, &CompactMailDelegate::flagRequested,
|
||||
this, &MailListView::onCompactFlagRequested);
|
||||
connect(m_compactDelegate, &CompactMailDelegate::deleteRequested,
|
||||
this, &MailListView::onCompactDeleteRequested);
|
||||
connect(m_compactDelegate, &CompactMailDelegate::categoryRequested,
|
||||
this, &MailListView::onCompactCategoryRequested);
|
||||
connect(m_compactDelegate, &CompactMailDelegate::moreRequested,
|
||||
this, &MailListView::onCompactMoreRequested);
|
||||
connect(m_compactDelegate, &CompactMailDelegate::mailClicked,
|
||||
this, &MailListView::onCompactMailClicked);
|
||||
|
||||
// Same proxy model for list view (single column)
|
||||
m_listView->setModel(m_proxyModel);
|
||||
m_listView->setModelColumn(0); // Use first column (subject)
|
||||
|
||||
// Add both views to stack
|
||||
m_viewStack->addWidget(m_treeView); // index 0 = normal
|
||||
m_viewStack->addWidget(m_listView); // index 1 = compact
|
||||
|
||||
// Show normal by default
|
||||
m_viewStack->setCurrentIndex(0);
|
||||
|
||||
// Connect filter checkboxes
|
||||
connect(m_unreadOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
@@ -136,28 +180,57 @@ void MailListView::setupUI() {
|
||||
connect(m_hasAttachmentsCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
}
|
||||
|
||||
void MailListView::setModel(EmailListModel *model) {
|
||||
void MailListView::setModel(EmailListModel *model)
|
||||
{
|
||||
m_sourceModel = model;
|
||||
|
||||
// Connect to source model changes to refresh tree
|
||||
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &MailListView::refreshTreeModel);
|
||||
connect(m_sourceModel, &QAbstractItemModel::layoutChanged, this, &MailListView::refreshTreeModel);
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &MailListView::refreshTreeModel);
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &MailListView::refreshTreeModel);
|
||||
|
||||
|
||||
// Connect to source model changes to refresh both views
|
||||
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &MailListView::refreshViews);
|
||||
connect(m_sourceModel, &QAbstractItemModel::layoutChanged, this, &MailListView::refreshViews);
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &MailListView::refreshViews);
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &MailListView::refreshViews);
|
||||
|
||||
// Initial population
|
||||
refreshTreeModel();
|
||||
refreshViews();
|
||||
}
|
||||
|
||||
void MailListView::refreshTreeModel() {
|
||||
void MailListView::refreshViews()
|
||||
{
|
||||
if (!m_sourceModel) return;
|
||||
|
||||
|
||||
const QVector<MailItem> &emails = m_sourceModel->emails();
|
||||
m_treeModel->setEmails(emails);
|
||||
m_treeView->expandAll();
|
||||
|
||||
// List view uses same proxy model, just refresh
|
||||
m_listView->update();
|
||||
}
|
||||
|
||||
void MailListView::onRowSelected(const QModelIndex &index) {
|
||||
void MailListView::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateViewMode();
|
||||
}
|
||||
|
||||
void MailListView::updateViewMode()
|
||||
{
|
||||
int viewportWidth = m_viewStack->width(); // available width
|
||||
bool wantCompact = (viewportWidth < m_compactThreshold);
|
||||
|
||||
if (wantCompact != m_compactMode) {
|
||||
m_compactMode = wantCompact;
|
||||
m_viewStack->setCurrentIndex(m_compactMode ? 1 : 0);
|
||||
|
||||
if (m_compactMode) {
|
||||
m_treeView->header()->hide();
|
||||
} else {
|
||||
m_treeView->header()->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MailListView::onRowSelected(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid()) return;
|
||||
QModelIndex proxyIdx = m_proxyModel->mapToSource(index);
|
||||
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::MailIdRole).isValid()) {
|
||||
@@ -166,20 +239,47 @@ void MailListView::onRowSelected(const QModelIndex &index) {
|
||||
}
|
||||
}
|
||||
|
||||
void MailListView::onSearchTextChanged(const QString &text) {
|
||||
void MailListView::onSearchTextChanged(const QString &text)
|
||||
{
|
||||
m_proxyModel->setFilterFixedString(text);
|
||||
}
|
||||
|
||||
void MailListView::onFilterChanged() {
|
||||
void MailListView::onFilterChanged()
|
||||
{
|
||||
if (!m_sourceModel) return;
|
||||
m_sourceModel->setShowUnreadOnly(m_unreadOnlyCheck->isChecked());
|
||||
m_sourceModel->setShowFlaggedOnly(m_flaggedOnlyCheck->isChecked());
|
||||
m_sourceModel->setShowHasAttachments(m_hasAttachmentsCheck->isChecked());
|
||||
// refreshTreeModel will be called automatically via modelReset signal
|
||||
}
|
||||
|
||||
void MailListView::onGroupByChanged(int index) {
|
||||
void MailListView::onGroupByChanged(int index)
|
||||
{
|
||||
bool enabled = (index == 1); // 0 = Sin agrupación, 1 = Agrupar por fecha
|
||||
m_treeModel->setGroupMode(enabled ? EmailTreeModel::GroupByDate : EmailTreeModel::NoGrouping);
|
||||
m_treeView->expandAll();
|
||||
}
|
||||
|
||||
void MailListView::onCompactFlagRequested(int mailId)
|
||||
{
|
||||
emit flagRequested(mailId);
|
||||
}
|
||||
|
||||
void MailListView::onCompactDeleteRequested(int mailId)
|
||||
{
|
||||
emit deleteRequested(mailId);
|
||||
}
|
||||
|
||||
void MailListView::onCompactCategoryRequested(int mailId)
|
||||
{
|
||||
emit categoryRequested(mailId);
|
||||
}
|
||||
|
||||
void MailListView::onCompactMoreRequested(int mailId, const QPoint &globalPos)
|
||||
{
|
||||
emit moreRequested(mailId, globalPos);
|
||||
}
|
||||
|
||||
void MailListView::onCompactMailClicked(int mailId)
|
||||
{
|
||||
emit emailSelected(mailId);
|
||||
}
|
||||
+24
-1
@@ -11,9 +11,13 @@
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QResizeEvent>
|
||||
#include <QListView>
|
||||
#include <QStackedWidget>
|
||||
|
||||
#include "ui/models/EmailListModel.h"
|
||||
#include "ui/models/EmailTreeModel.h"
|
||||
#include "ui/delegates/CompactMailDelegate.h"
|
||||
|
||||
class MailListView : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -24,23 +28,39 @@ public:
|
||||
|
||||
void setModel(EmailListModel *model);
|
||||
QTreeView* treeView() const { return m_treeView; }
|
||||
QListView* listView() const { return m_listView; }
|
||||
|
||||
signals:
|
||||
void emailSelected(int mailId);
|
||||
void emailOpenRequested(int mailId);
|
||||
void composeRequested();
|
||||
void flagRequested(int mailId);
|
||||
void deleteRequested(int mailId);
|
||||
void categoryRequested(int mailId);
|
||||
void moreRequested(int mailId, const QPoint& globalPos);
|
||||
|
||||
private slots:
|
||||
void onRowSelected(const QModelIndex &index);
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onFilterChanged();
|
||||
void onGroupByChanged(int index);
|
||||
void onCompactFlagRequested(int mailId);
|
||||
void onCompactDeleteRequested(int mailId);
|
||||
void onCompactCategoryRequested(int mailId);
|
||||
void onCompactMoreRequested(int mailId, const QPoint& globalPos);
|
||||
void onCompactMailClicked(int mailId);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void refreshTreeModel();
|
||||
void refreshViews();
|
||||
void updateViewMode();
|
||||
|
||||
QTreeView *m_treeView;
|
||||
QListView *m_listView;
|
||||
QStackedWidget *m_viewStack;
|
||||
EmailTreeModel *m_treeModel;
|
||||
QSortFilterProxyModel *m_proxyModel;
|
||||
EmailListModel *m_sourceModel = nullptr;
|
||||
@@ -50,4 +70,7 @@ private:
|
||||
QCheckBox *m_flaggedOnlyCheck;
|
||||
QCheckBox *m_hasAttachmentsCheck;
|
||||
QComboBox *m_groupByCombo;
|
||||
CompactMailDelegate *m_compactDelegate = nullptr;
|
||||
bool m_compactMode = false;
|
||||
int m_compactThreshold = 420;
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "services/rulesengine.h"
|
||||
#include "ui/rulesmanagerdialog.h"
|
||||
#include "ui/accountsetupdialog.h"
|
||||
#include "ui/delegates/FolderTreeDelegate.h"
|
||||
#include <optional>
|
||||
#include <QMessageBox>
|
||||
#include <QFileDialog>
|
||||
@@ -227,6 +228,7 @@ void MainMainWindow::setupMailPage()
|
||||
m_folderTree->setMaximumWidth(350);
|
||||
m_folderTree->setFrameShape(QFrame::NoFrame);
|
||||
m_folderTree->setExpandsOnDoubleClick(true);
|
||||
m_folderTree->setItemDelegate(new FolderTreeDelegate(this)); // Custom delegate with unread count badges
|
||||
leftSplitter->addWidget(m_folderTree);
|
||||
|
||||
// Category tree
|
||||
@@ -255,6 +257,10 @@ void MainMainWindow::setupMailPage()
|
||||
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
|
||||
openMailInIndependentWindow(mailId);
|
||||
});
|
||||
connect(m_mailListView, &MailListView::flagRequested, this, &MainMainWindow::onFlagRequested);
|
||||
connect(m_mailListView, &MailListView::deleteRequested, this, &MainMainWindow::onDeleteRequested);
|
||||
connect(m_mailListView, &MailListView::categoryRequested, this, &MainMainWindow::onCategoryRequested);
|
||||
connect(m_mailListView, &MailListView::moreRequested, this, &MainMainWindow::onMoreRequested);
|
||||
m_folderSplitter->addWidget(m_mailListView);
|
||||
|
||||
// Viewer stack: placeholder, reader, compose
|
||||
@@ -466,6 +472,59 @@ void MainMainWindow::onReaderDeleteRequested(int mailId)
|
||||
statusBar()->showMessage(tr("Mensaje eliminado"), 3000);
|
||||
}
|
||||
|
||||
// MailListView action handlers (from compact delegate)
|
||||
void MainMainWindow::onFlagRequested(int mailId)
|
||||
{
|
||||
auto item = MailItemDao::findById(mailId);
|
||||
if (item.has_value()) {
|
||||
item->setFlagged(!item->isFlagged());
|
||||
MailItemDao::update(*item);
|
||||
m_emailModel->refresh();
|
||||
if (m_currentMailId == mailId) {
|
||||
m_emailViewer->setMailItem(&*item);
|
||||
}
|
||||
statusBar()->showMessage(tr(item->isFlagged() ? "Marcado para seguimiento" : "Seguimiento quitado"), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
void MainMainWindow::onDeleteRequested(int mailId)
|
||||
{
|
||||
MailItemDao::remove(mailId);
|
||||
if (m_currentMailId == mailId) {
|
||||
m_currentMailId = -1;
|
||||
m_viewerStack->setCurrentIndex(0); // placeholder
|
||||
}
|
||||
m_emailModel->refresh();
|
||||
statusBar()->showMessage(tr("Mensaje eliminado"), 3000);
|
||||
}
|
||||
|
||||
void MainMainWindow::onCategoryRequested(int mailId)
|
||||
{
|
||||
// Show category context menu at cursor position
|
||||
// For now, just show status - could open a dialog
|
||||
statusBar()->showMessage(tr("Categoría para mensaje %1").arg(mailId), 2000);
|
||||
}
|
||||
|
||||
void MainMainWindow::onMoreRequested(int mailId, const QPoint &globalPos)
|
||||
{
|
||||
// Show context menu with more actions
|
||||
QMenu menu;
|
||||
menu.addAction("Responder", [this, mailId]() { onReaderReplyRequested(mailId); });
|
||||
menu.addAction("Reenviar", [this, mailId]() { onReaderForwardRequested(mailId); });
|
||||
menu.addAction("Mover a...", [this, mailId]() { /* TODO: move dialog */ });
|
||||
menu.addAction("Marcar como no leído", [this, mailId]() {
|
||||
auto item = MailItemDao::findById(mailId);
|
||||
if (item.has_value()) {
|
||||
item->setRead(false);
|
||||
MailItemDao::update(*item);
|
||||
m_emailModel->refresh();
|
||||
}
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction("Eliminar", [this, mailId]() { onDeleteRequested(mailId); });
|
||||
menu.exec(globalPos);
|
||||
}
|
||||
|
||||
void MainMainWindow::onNewMessage()
|
||||
{ // Show compose view in the viewer stack
|
||||
m_embeddedComposeView->initializeComposition();
|
||||
|
||||
@@ -74,6 +74,12 @@ private slots:
|
||||
void showRulesManager();
|
||||
void showTemplatesManager();
|
||||
|
||||
// MailListView action handlers (from compact delegate)
|
||||
void onFlagRequested(int mailId);
|
||||
void onDeleteRequested(int mailId);
|
||||
void onCategoryRequested(int mailId);
|
||||
void onMoreRequested(int mailId, const QPoint &globalPos);
|
||||
|
||||
// Slots for embedded compose view
|
||||
void onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
|
||||
const QString &subject, const QString &body,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "FolderListModel.h"
|
||||
#include <QDebug>
|
||||
#include "db/dao/mailitemdao.h"
|
||||
|
||||
// --- FolderTreeItem ---
|
||||
|
||||
@@ -156,8 +157,7 @@ QVariant FolderListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == UnreadCountRole) {
|
||||
if (item->type() == FolderTreeItem::FolderNode) {
|
||||
Folder folder = item->data().value<Folder>();
|
||||
// TODO: compute unread count
|
||||
return 0;
|
||||
return MailItemDao::countUnreadByFolderId(folder.id());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user