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
+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);
}