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:
2026-08-24 23:25:13 +02:00
parent 705fe32e66
commit 4c4fec7a0a
846 changed files with 498002 additions and 52534 deletions
+16
View File
@@ -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;
}
+1
View File
@@ -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);
};
+333
View File
@@ -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);
}
+87
View File
@@ -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;
};
+104
View File
@@ -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
}
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+59
View File
@@ -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();
+6
View File
@@ -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,
+2 -2
View File
@@ -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());
}
}