feat: email grouping by date + stability fixes
- EmailTreeModel: hierarchical model for grouped email view (QTreeView) - DateGroupProxyModel: proxy model for date-based grouping logic - MailListView: migrated from QTableView to QTreeView with grouping support - ReaderView: subject frame with shadow, avatar moved to header, body inside header - MainWindow: frameless with rounded corners, 1px border, edge resize, no status bar - Stability fixes for grouping mode switch: * Proper tree cleanup (no double-delete of root) * Bounds checking on all array accesses * Null pointer checks in index/parent/data methods * Date validity checks before grouping * Index validation before accessing m_emails
This commit is contained in:
+101
-74
@@ -1,7 +1,18 @@
|
||||
#include "ui/maillistview.h"
|
||||
#include <QDateTime>
|
||||
#include "ui/models/EmailListModel.h"
|
||||
#include "ui/models/EmailTreeModel.h"
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QHeaderView>
|
||||
|
||||
MailListView::MailListView(QWidget *parent) : QWidget(parent) {
|
||||
MailListView::MailListView(QWidget *parent) : QWidget(parent), m_sourceModel(nullptr) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
@@ -10,29 +21,6 @@ void MailListView::setupUI() {
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
// Header bar
|
||||
QWidget *headerBar = new QWidget();
|
||||
headerBar->setFixedHeight(48);
|
||||
headerBar->setStyleSheet("background-color: #1976D2;");
|
||||
QHBoxLayout *headerLayout = new QHBoxLayout(headerBar);
|
||||
headerLayout->setContentsMargins(16, 0, 10, 0);
|
||||
|
||||
QLabel *title = new QLabel("Wino Mail");
|
||||
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
headerLayout->addWidget(title);
|
||||
headerLayout->addStretch();
|
||||
|
||||
m_composeButton = new QPushButton("✎");
|
||||
m_composeButton->setFixedSize(36, 36);
|
||||
m_composeButton->setStyleSheet(
|
||||
"QPushButton { background-color: #e0e0e0; border-radius: 4px; font-size: 18px; color: #333; }"
|
||||
"QPushButton:hover { background-color: #d0d0d0; }"
|
||||
);
|
||||
headerLayout->addWidget(m_composeButton);
|
||||
connect(m_composeButton, &QPushButton::clicked, this, &MailListView::composeRequested);
|
||||
|
||||
layout->addWidget(headerBar);
|
||||
|
||||
// Search bar
|
||||
QWidget *searchBar = new QWidget();
|
||||
searchBar->setFixedHeight(44);
|
||||
@@ -76,83 +64,122 @@ void MailListView::setupUI() {
|
||||
|
||||
filterLayout->addStretch();
|
||||
|
||||
connect(m_unreadOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
connect(m_flaggedOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
connect(m_hasAttachmentsCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
// Group by combo
|
||||
m_groupByCombo = new QComboBox();
|
||||
m_groupByCombo->addItem("Sin agrupación");
|
||||
m_groupByCombo->addItem("Agrupar por fecha");
|
||||
m_groupByCombo->setFixedWidth(180);
|
||||
m_groupByCombo->setStyleSheet(
|
||||
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 4px 8px; "
|
||||
"background: white; min-height: 20px; }"
|
||||
"QComboBox:hover { border-color: #bdbdbd; }"
|
||||
"QComboBox:focus { border-color: #1976D2; }"
|
||||
);
|
||||
connect(m_groupByCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &MailListView::onGroupByChanged);
|
||||
filterLayout->addWidget(m_groupByCombo);
|
||||
|
||||
layout->addWidget(filterBar);
|
||||
|
||||
// Table
|
||||
m_tableView = new QTableView();
|
||||
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableView->setShowGrid(false);
|
||||
m_tableView->setAlternatingRowColors(true);
|
||||
m_tableView->verticalHeader()->hide();
|
||||
m_tableView->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableView->horizontalHeader()->setSectionsClickable(true);
|
||||
m_tableView->setSortingEnabled(true);
|
||||
m_tableView->setFrameShape(QFrame::NoFrame);
|
||||
m_tableView->setStyleSheet(
|
||||
"QTableView { background-color: #ffffff; alternate-background-color: #f9f9fb; border: none; }"
|
||||
"QTableView::item { padding: 8px; border-bottom: 1px solid #e8e8ed; }"
|
||||
"QTableView::item:selected { background-color: #e3f2fd; color: #1a1a2e; }"
|
||||
// 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->setFrameShape(QFrame::NoFrame);
|
||||
m_treeView->setRootIsDecorated(true);
|
||||
m_treeView->setItemsExpandable(true);
|
||||
m_treeView->setUniformRowHeights(true);
|
||||
m_treeView->setStyleSheet(
|
||||
"QTreeView { background-color: #ffffff; alternate-background-color: #f9f9fb; border: none; }"
|
||||
"QTreeView::item { padding: 8px; border-bottom: 1px solid #e8e8ed; }"
|
||||
"QTreeView::item:selected { background-color: #e3f2fd; color: #1a1a2e; }"
|
||||
"QTreeView::branch { background: transparent; }"
|
||||
"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->setSortRole(EmailListModel::DateRole);
|
||||
m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_proxyModel->setDynamicSortFilter(true);
|
||||
m_proxyModel->setSourceModel(m_treeModel);
|
||||
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_proxyModel->setFilterKeyColumn(-1); // Search all columns
|
||||
|
||||
m_tableView->setModel(m_proxyModel);
|
||||
m_treeView->setModel(m_proxyModel);
|
||||
|
||||
connect(m_tableView, &QTableView::clicked, this, &MailListView::onRowSelected);
|
||||
connect(m_tableView, &QTableView::doubleClicked, this, [this](const QModelIndex &index) {
|
||||
// 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);
|
||||
|
||||
connect(m_treeView, &QTreeView::clicked, this, &MailListView::onRowSelected);
|
||||
connect(m_treeView, &QTreeView::doubleClicked, this, [this](const QModelIndex &index) {
|
||||
if (!index.isValid()) return;
|
||||
QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
|
||||
int mailId = sourceIndex.data(EmailListModel::IdRole).toInt();
|
||||
emit emailOpenRequested(mailId);
|
||||
QModelIndex proxyIdx = m_proxyModel->mapToSource(index);
|
||||
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::MailIdRole).isValid()) {
|
||||
int mailId = proxyIdx.data(EmailTreeModel::MailIdRole).toInt();
|
||||
emit emailOpenRequested(mailId);
|
||||
}
|
||||
});
|
||||
|
||||
layout->addWidget(m_tableView);
|
||||
// Expand all groups by default
|
||||
m_treeView->expandAll();
|
||||
|
||||
layout->addWidget(m_treeView);
|
||||
|
||||
// Connect filter checkboxes
|
||||
connect(m_unreadOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
connect(m_flaggedOnlyCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
connect(m_hasAttachmentsCheck, &QCheckBox::toggled, this, &MailListView::onFilterChanged);
|
||||
}
|
||||
|
||||
void MailListView::setModel(EmailListModel *model) {
|
||||
m_proxyModel->setSourceModel(model);
|
||||
|
||||
// Hide columns we don't want to show (by index now, not by role)
|
||||
// Columns: 0=Subject, 1=Sender, 2=Date
|
||||
// All visible for now - we can hide via header data
|
||||
m_sourceModel = model;
|
||||
|
||||
// Default sort by date descending (column 2)
|
||||
m_tableView->sortByColumn(EmailListModel::ColDate, Qt::DescendingOrder);
|
||||
// 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);
|
||||
|
||||
// Initial population
|
||||
refreshTreeModel();
|
||||
}
|
||||
|
||||
// Set column resize modes
|
||||
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::ColSubject, QHeaderView::Stretch);
|
||||
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::ColSender, QHeaderView::Stretch);
|
||||
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::ColDate, QHeaderView::ResizeToContents);
|
||||
void MailListView::refreshTreeModel() {
|
||||
if (!m_sourceModel) return;
|
||||
|
||||
const QVector<MailItem> &emails = m_sourceModel->emails();
|
||||
m_treeModel->setEmails(emails);
|
||||
m_treeView->expandAll();
|
||||
}
|
||||
|
||||
void MailListView::onRowSelected(const QModelIndex &index) {
|
||||
if (!index.isValid()) return;
|
||||
QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
|
||||
int mailId = sourceIndex.data(EmailListModel::IdRole).toInt();
|
||||
emit emailSelected(mailId);
|
||||
QModelIndex proxyIdx = m_proxyModel->mapToSource(index);
|
||||
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::MailIdRole).isValid()) {
|
||||
int mailId = proxyIdx.data(EmailTreeModel::MailIdRole).toInt();
|
||||
emit emailSelected(mailId);
|
||||
}
|
||||
}
|
||||
|
||||
void MailListView::onSearchTextChanged(const QString &text) {
|
||||
if (auto *model = qobject_cast<EmailListModel*>(m_proxyModel->sourceModel())) {
|
||||
model->setSearchFilter(text);
|
||||
}
|
||||
m_proxyModel->setFilterFixedString(text);
|
||||
}
|
||||
|
||||
void MailListView::onFilterChanged() {
|
||||
if (auto *model = qobject_cast<EmailListModel*>(m_proxyModel->sourceModel())) {
|
||||
model->setShowUnreadOnly(m_unreadOnlyCheck->isChecked());
|
||||
model->setShowFlaggedOnly(m_flaggedOnlyCheck->isChecked());
|
||||
model->setShowHasAttachments(m_hasAttachmentsCheck->isChecked());
|
||||
}
|
||||
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) {
|
||||
bool enabled = (index == 1); // 0 = Sin agrupación, 1 = Agrupar por fecha
|
||||
m_treeModel->setGroupMode(enabled ? EmailTreeModel::GroupByDate : EmailTreeModel::NoGrouping);
|
||||
m_treeView->expandAll();
|
||||
}
|
||||
Reference in New Issue
Block a user