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();
|
||||
}
|
||||
+10
-3
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTableView>
|
||||
#include <QTreeView>
|
||||
#include <QHeaderView>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVBoxLayout>
|
||||
@@ -10,8 +10,10 @@
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
|
||||
#include "ui/models/EmailListModel.h"
|
||||
#include "ui/models/EmailTreeModel.h"
|
||||
|
||||
class MailListView : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -21,7 +23,7 @@ public:
|
||||
~MailListView() override = default;
|
||||
|
||||
void setModel(EmailListModel *model);
|
||||
QTableView* tableView() const { return m_tableView; }
|
||||
QTreeView* treeView() const { return m_treeView; }
|
||||
|
||||
signals:
|
||||
void emailSelected(int mailId);
|
||||
@@ -32,15 +34,20 @@ private slots:
|
||||
void onRowSelected(const QModelIndex &index);
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onFilterChanged();
|
||||
void onGroupByChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void refreshTreeModel();
|
||||
|
||||
QTableView *m_tableView;
|
||||
QTreeView *m_treeView;
|
||||
EmailTreeModel *m_treeModel;
|
||||
QSortFilterProxyModel *m_proxyModel;
|
||||
EmailListModel *m_sourceModel = nullptr;
|
||||
QPushButton *m_composeButton;
|
||||
QLineEdit *m_searchEdit;
|
||||
QCheckBox *m_unreadOnlyCheck;
|
||||
QCheckBox *m_flaggedOnlyCheck;
|
||||
QCheckBox *m_hasAttachmentsCheck;
|
||||
QComboBox *m_groupByCombo;
|
||||
};
|
||||
@@ -385,7 +385,7 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
|
||||
m_emailModel->refresh();
|
||||
// Clear selection and show placeholder
|
||||
m_currentMailId = -1;
|
||||
m_mailListView->tableView()->clearSelection();
|
||||
m_mailListView->treeView()->clearSelection();
|
||||
m_viewerStack->setCurrentIndex(0); // placeholder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "DateGroupProxyModel.h"
|
||||
#include <QDateTime>
|
||||
#include <QDate>
|
||||
#include <QDebug>
|
||||
#include "EmailListModel.h"
|
||||
|
||||
DateGroupProxyModel::DateGroupProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
void DateGroupProxyModel::setGroupByDate(bool enabled)
|
||||
{
|
||||
if (m_groupByDate == enabled) return;
|
||||
m_groupByDate = enabled;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
bool DateGroupProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
if (!m_groupByDate) {
|
||||
return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
|
||||
}
|
||||
|
||||
// When grouping, we don't filter rows, we just organize them
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DateGroupProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
{
|
||||
// First sort by group, then by date within group (descending)
|
||||
if (m_groupByDate) {
|
||||
int leftGroup = groupForRow(left.row());
|
||||
int rightGroup = groupForRow(right.row());
|
||||
|
||||
if (leftGroup != rightGroup) {
|
||||
return leftGroup < rightGroup;
|
||||
}
|
||||
|
||||
// Same group - sort by date descending (newest first)
|
||||
QVariant leftDate = sourceModel()->data(left, EmailListModel::DateRole);
|
||||
QVariant rightDate = sourceModel()->data(right, EmailListModel::DateRole);
|
||||
|
||||
if (leftDate.isValid() && rightDate.isValid()) {
|
||||
return leftDate.toDateTime() > rightDate.toDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
|
||||
QString DateGroupProxyModel::groupNameForDate(const QDateTime &date)
|
||||
{
|
||||
if (!date.isValid()) return "Sin fecha";
|
||||
|
||||
QDate emailDate = date.date();
|
||||
QDate today = QDate::currentDate();
|
||||
|
||||
int daysDiff = today.daysTo(emailDate);
|
||||
|
||||
if (daysDiff == 0) {
|
||||
return "Hoy";
|
||||
} else if (daysDiff == -1) {
|
||||
return "Ayer";
|
||||
} else if (daysDiff > -7 && daysDiff < 0) {
|
||||
// This week (Monday to Sunday)
|
||||
return emailDate.dayOfWeek() == 1 ? "Esta semana (Lunes)" : "Esta semana";
|
||||
} else if (daysDiff >= -13 && daysDiff <= -7) {
|
||||
return "Semana pasada";
|
||||
} else if (daysDiff >= -20 && daysDiff <= -14) {
|
||||
return "Hace dos semanas";
|
||||
} else if (emailDate.month() == today.month() && emailDate.year() == today.year()) {
|
||||
return "Este mes";
|
||||
} else if (emailDate.year() == today.year()) {
|
||||
// Month name
|
||||
static const QString months[] = {
|
||||
"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
||||
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
|
||||
};
|
||||
return months[emailDate.month() - 1];
|
||||
} else {
|
||||
return QString("%1").arg(emailDate.year());
|
||||
}
|
||||
}
|
||||
|
||||
void DateGroupProxyModel::updateGroupCache() const
|
||||
{
|
||||
m_groupRowCounts.clear();
|
||||
m_groupStartRows.clear();
|
||||
|
||||
int rowCount = this->rowCount();
|
||||
if (rowCount == 0) return;
|
||||
|
||||
QString lastGroup = "";
|
||||
int currentGroup = -1;
|
||||
|
||||
for (int i = 0; i < rowCount; ++i) {
|
||||
QModelIndex idx = index(i, 0);
|
||||
QString group = idx.data(GroupRole).toString();
|
||||
|
||||
if (group != lastGroup) {
|
||||
currentGroup++;
|
||||
lastGroup = group;
|
||||
m_groupStartRows.append(i);
|
||||
m_groupRowCounts.append(1);
|
||||
} else {
|
||||
m_groupRowCounts[currentGroup]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int DateGroupProxyModel::groupForRow(int row) const
|
||||
{
|
||||
if (row < 0 || row >= rowCount()) return -1;
|
||||
|
||||
if (m_groupStartRows.isEmpty()) {
|
||||
updateGroupCache();
|
||||
}
|
||||
|
||||
// Binary search for group
|
||||
int group = 0;
|
||||
for (int i = m_groupStartRows.size() - 1; i >= 0; --i) {
|
||||
if (row >= m_groupStartRows[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString DateGroupProxyModel::groupNameForRow(int row) const
|
||||
{
|
||||
if (row < 0 || row >= rowCount()) return "";
|
||||
|
||||
QModelIndex idx = index(row, 0);
|
||||
return idx.data(GroupRole).toString();
|
||||
}
|
||||
|
||||
QVariant DateGroupProxyModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) return QVariant();
|
||||
|
||||
if (role == GroupRole) {
|
||||
// Return group name for this row
|
||||
QModelIndex sourceIdx = mapToSource(index);
|
||||
QVariant dateVar = sourceModel()->data(sourceIdx, EmailListModel::DateRole);
|
||||
if (dateVar.isValid()) {
|
||||
return groupNameForDate(dateVar.toDateTime());
|
||||
}
|
||||
return "Sin fecha";
|
||||
}
|
||||
|
||||
if (role == IsGroupHeaderRole) {
|
||||
// This would be used if we inserted header rows - not implemented yet
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == GroupSectionRole) {
|
||||
return groupForRow(index.row());
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
QVariant DateGroupProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == GroupRole && orientation == Qt::Horizontal && m_groupByDate) {
|
||||
// For the date column, we might want to show group headers
|
||||
// This is for column headers, not row grouping
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::headerData(section, orientation, role);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QDateTime>
|
||||
#include <QVector>
|
||||
#include <QPair>
|
||||
|
||||
class DateGroupProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum GroupBy {
|
||||
GroupByDate = 0
|
||||
};
|
||||
|
||||
explicit DateGroupProxyModel(QObject *parent = nullptr);
|
||||
|
||||
// Grouping methods
|
||||
void setGroupByDate(bool enabled);
|
||||
bool isGroupByDateEnabled() const { return m_groupByDate; }
|
||||
|
||||
// Override to provide group headers
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// For grouping - we'll use a custom role for group identification
|
||||
static constexpr int GroupRole = Qt::UserRole + 100;
|
||||
static constexpr int IsGroupHeaderRole = Qt::UserRole + 101;
|
||||
static constexpr int GroupSectionRole = Qt::UserRole + 102;
|
||||
|
||||
// Helper to get group name for a date
|
||||
static QString groupNameForDate(const QDateTime &date);
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
private:
|
||||
bool m_groupByDate = false;
|
||||
mutable QVector<int> m_groupRowCounts; // cached group sizes
|
||||
mutable QVector<int> m_groupStartRows; // cached start rows for each group
|
||||
|
||||
void updateGroupCache() const;
|
||||
int groupForRow(int row) const;
|
||||
QString groupNameForRow(int row) const;
|
||||
};
|
||||
@@ -53,6 +53,9 @@ public:
|
||||
void setShowFlaggedOnly(bool show);
|
||||
void setShowHasAttachments(bool show);
|
||||
|
||||
// Get all emails currently in the model (after filtering)
|
||||
const QVector<MailItem>& emails() const { return m_emails; }
|
||||
|
||||
private:
|
||||
QVector<MailItem> m_emails;
|
||||
int m_folderId{-1}; // -1 means all folders
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
#include "EmailTreeModel.h"
|
||||
#include <QDateTime>
|
||||
#include <QDate>
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
|
||||
EmailTreeModel::EmailTreeModel(QObject *parent)
|
||||
: QAbstractItemModel(parent), m_rootItem(new TreeItem(TreeItem::Group))
|
||||
{
|
||||
m_rootItem->type = TreeItem::Group;
|
||||
m_rootItem->groupName = "Root";
|
||||
}
|
||||
|
||||
EmailTreeModel::~EmailTreeModel()
|
||||
{
|
||||
deleteTree(m_rootItem);
|
||||
}
|
||||
|
||||
void EmailTreeModel::deleteTree(TreeItem* item)
|
||||
{
|
||||
if (!item) return;
|
||||
qDeleteAll(item->children);
|
||||
delete item;
|
||||
}
|
||||
|
||||
QModelIndex EmailTreeModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent))
|
||||
return QModelIndex();
|
||||
|
||||
TreeItem* parentItem;
|
||||
if (!parent.isValid())
|
||||
parentItem = m_rootItem;
|
||||
else
|
||||
parentItem = static_cast<TreeItem*>(parent.internalPointer());
|
||||
|
||||
// Verify parentItem is valid
|
||||
if (!parentItem)
|
||||
return QModelIndex();
|
||||
|
||||
if (row < 0 || row >= parentItem->children.size())
|
||||
return QModelIndex();
|
||||
|
||||
TreeItem* childItem = parentItem->children[row];
|
||||
if (!childItem)
|
||||
return QModelIndex();
|
||||
|
||||
return createIndex(row, column, childItem);
|
||||
}
|
||||
|
||||
QModelIndex EmailTreeModel::parent(const QModelIndex &child) const
|
||||
{
|
||||
if (!child.isValid())
|
||||
return QModelIndex();
|
||||
|
||||
TreeItem* childItem = static_cast<TreeItem*>(child.internalPointer());
|
||||
if (!childItem)
|
||||
return QModelIndex();
|
||||
|
||||
TreeItem* parentItem = childItem->parent;
|
||||
if (!parentItem || parentItem == m_rootItem)
|
||||
return QModelIndex();
|
||||
|
||||
return createIndex(parentItem->row, 0, parentItem);
|
||||
}
|
||||
|
||||
int EmailTreeModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.column() > 0)
|
||||
return 0;
|
||||
|
||||
TreeItem* parentItem;
|
||||
if (!parent.isValid())
|
||||
parentItem = m_rootItem;
|
||||
else
|
||||
parentItem = static_cast<TreeItem*>(parent.internalPointer());
|
||||
|
||||
return parentItem->children.size();
|
||||
}
|
||||
|
||||
int EmailTreeModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return ColCount;
|
||||
}
|
||||
|
||||
QVariant EmailTreeModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
TreeItem* item = static_cast<TreeItem*>(index.internalPointer());
|
||||
|
||||
if (role == MailIdRole) {
|
||||
if (item->type == TreeItem::Mail) {
|
||||
const MailItem& mail = m_emails[item->mailIndex];
|
||||
return mail.id();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (role == IsGroupRole) {
|
||||
return item->type == TreeItem::Group;
|
||||
}
|
||||
|
||||
if (role == GroupNameRole && item->type == TreeItem::Group) {
|
||||
return item->groupName;
|
||||
}
|
||||
|
||||
if (role == GroupIndexRole && item->type == TreeItem::Group) {
|
||||
return item->groupIndex;
|
||||
}
|
||||
|
||||
if (role == MailIndexRole && item->type == TreeItem::Mail) {
|
||||
return item->mailIndex;
|
||||
}
|
||||
|
||||
if (role != Qt::DisplayRole && role != Qt::ToolTipRole)
|
||||
return QVariant();
|
||||
|
||||
if (item->type == TreeItem::Group) {
|
||||
if (index.column() == ColSubject) {
|
||||
return QString("%1 (%2)").arg(item->groupName).arg(item->children.size());
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// Mail item
|
||||
if (item->type == TreeItem::Mail) {
|
||||
if (item->mailIndex < 0 || item->mailIndex >= m_emails.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
const MailItem& mail = m_emails[item->mailIndex];
|
||||
|
||||
switch (index.column()) {
|
||||
case ColSubject:
|
||||
return mail.subject().isEmpty() ? "(Sin asunto)" : mail.subject();
|
||||
case ColSender:
|
||||
return mail.sender();
|
||||
case ColDate: {
|
||||
if (!mail.date().isValid()) return QVariant();
|
||||
QDateTime dt = mail.date();
|
||||
QDate today = QDate::currentDate();
|
||||
if (dt.date() == today) {
|
||||
return dt.toString("HH:mm");
|
||||
} else if (dt.date() == today.addDays(-1)) {
|
||||
return "Ayer " + dt.toString("HH:mm");
|
||||
} else if (dt.date().year() == today.year()) {
|
||||
return dt.toString("ddd dd/MM HH:mm");
|
||||
} else {
|
||||
return dt.toString("dd/MM/yyyy HH:mm");
|
||||
}
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant EmailTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
|
||||
switch (section) {
|
||||
case ColSubject: return "Asunto";
|
||||
case ColSender: return "De";
|
||||
case ColDate: return "Fecha";
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
return QAbstractItemModel::headerData(section, orientation, role);
|
||||
}
|
||||
|
||||
Qt::ItemFlags EmailTreeModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
TreeItem* item = static_cast<TreeItem*>(index.internalPointer());
|
||||
|
||||
Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
if (item->type == TreeItem::Group) {
|
||||
// Groups don't need special flags for expand/collapse in Qt6
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
void EmailTreeModel::setEmails(const QVector<MailItem> &emails)
|
||||
{
|
||||
beginResetModel();
|
||||
m_emails = emails;
|
||||
// Clear tree properly without deleting root
|
||||
qDeleteAll(m_rootItem->children);
|
||||
m_rootItem->children.clear();
|
||||
setupModelData();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void EmailTreeModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_emails.clear();
|
||||
deleteTree(m_rootItem);
|
||||
m_rootItem = new TreeItem(TreeItem::Group);
|
||||
m_rootItem->type = TreeItem::Group;
|
||||
m_rootItem->groupName = "Root";
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void EmailTreeModel::setGroupMode(GroupMode mode)
|
||||
{
|
||||
if (m_groupMode == mode) return;
|
||||
|
||||
// Clear existing tree first
|
||||
beginResetModel();
|
||||
m_groupMode = mode;
|
||||
qDeleteAll(m_rootItem->children);
|
||||
m_rootItem->children.clear();
|
||||
setupModelData();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void EmailTreeModel::setupModelData()
|
||||
{
|
||||
if (m_groupMode == NoGrouping) {
|
||||
// Flat list - all mails directly under root
|
||||
for (int i = 0; i < m_emails.size(); ++i) {
|
||||
if (i < 0 || i >= m_emails.size()) continue;
|
||||
TreeItem* mailItem = new TreeItem(TreeItem::Mail, m_rootItem);
|
||||
mailItem->mailIndex = i;
|
||||
mailItem->row = m_rootItem->children.size();
|
||||
m_rootItem->children.append(mailItem);
|
||||
}
|
||||
} else {
|
||||
// Grouped by date
|
||||
buildGroups();
|
||||
}
|
||||
}
|
||||
|
||||
void EmailTreeModel::buildGroups()
|
||||
{
|
||||
// Sort emails by date descending (newest first)
|
||||
QVector<int> indices(m_emails.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
std::sort(indices.begin(), indices.end(), [this](int a, int b) {
|
||||
const QDateTime& dateA = m_emails[a].date();
|
||||
const QDateTime& dateB = m_emails[b].date();
|
||||
if (!dateA.isValid() && !dateB.isValid()) return false;
|
||||
if (!dateA.isValid()) return false;
|
||||
if (!dateB.isValid()) return true;
|
||||
return dateA > dateB;
|
||||
});
|
||||
|
||||
// Group by date
|
||||
QMap<QString, QVector<int>> groups; // groupName -> mail indices
|
||||
for (int idx : indices) {
|
||||
if (idx < 0 || idx >= m_emails.size()) continue;
|
||||
const QDateTime& dt = m_emails[idx].date();
|
||||
if (!dt.isValid()) continue;
|
||||
|
||||
QString groupName = groupNameForDate(dt);
|
||||
groups[groupName].append(idx);
|
||||
}
|
||||
|
||||
// Create tree items in group order
|
||||
int groupIdx = 0;
|
||||
for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) {
|
||||
const QString& groupName = it.key();
|
||||
const QVector<int>& mailIndices = it.value();
|
||||
if (mailIndices.isEmpty()) continue;
|
||||
|
||||
TreeItem* groupItem = new TreeItem(TreeItem::Group, m_rootItem);
|
||||
groupItem->groupName = groupName;
|
||||
groupItem->groupIndex = groupIdx++;
|
||||
groupItem->row = m_rootItem->children.size();
|
||||
m_rootItem->children.append(groupItem);
|
||||
|
||||
for (int mailIdx : mailIndices) {
|
||||
if (mailIdx < 0 || mailIdx >= m_emails.size()) continue;
|
||||
TreeItem* mailItem = new TreeItem(TreeItem::Mail, groupItem);
|
||||
mailItem->mailIndex = mailIdx;
|
||||
mailItem->row = groupItem->children.size();
|
||||
groupItem->children.append(mailItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString EmailTreeModel::groupNameForDate(const QDateTime &date) const
|
||||
{
|
||||
if (!date.isValid()) return "Sin fecha";
|
||||
|
||||
QDate emailDate = date.date();
|
||||
QDate today = QDate::currentDate();
|
||||
|
||||
int daysDiff = today.daysTo(emailDate);
|
||||
|
||||
if (daysDiff == 0) {
|
||||
return "Hoy";
|
||||
} else if (daysDiff == -1) {
|
||||
return "Ayer";
|
||||
} else if (daysDiff > -7 && daysDiff < 0) {
|
||||
// This week
|
||||
return "Esta semana";
|
||||
} else if (daysDiff >= -13 && daysDiff <= -7) {
|
||||
return "Semana pasada";
|
||||
} else if (daysDiff >= -20 && daysDiff <= -14) {
|
||||
return "Hace dos semanas";
|
||||
} else if (emailDate.month() == today.month() && emailDate.year() == today.year()) {
|
||||
return "Este mes";
|
||||
} else if (emailDate.year() == today.year()) {
|
||||
static const QString months[] = {
|
||||
"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
||||
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
|
||||
};
|
||||
return months[emailDate.month() - 1];
|
||||
} else {
|
||||
return QString("%1").arg(emailDate.year());
|
||||
}
|
||||
}
|
||||
|
||||
int EmailTreeModel::groupIndexForDate(const QDateTime &date) const
|
||||
{
|
||||
// Not used directly but kept for reference
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QModelIndex>
|
||||
#include <QVariant>
|
||||
#include <QVector>
|
||||
#include <QDateTime>
|
||||
#include <QDate>
|
||||
#include "core/mailitem.h"
|
||||
|
||||
class EmailTreeModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Columns {
|
||||
ColSubject = 0,
|
||||
ColSender = 1,
|
||||
ColDate = 2,
|
||||
ColCount = 3
|
||||
};
|
||||
|
||||
enum Roles {
|
||||
MailIdRole = Qt::UserRole + 1,
|
||||
IsGroupRole = Qt::UserRole + 2,
|
||||
GroupNameRole = Qt::UserRole + 3,
|
||||
GroupIndexRole = Qt::UserRole + 4,
|
||||
MailIndexRole = Qt::UserRole + 5
|
||||
};
|
||||
|
||||
explicit EmailTreeModel(QObject *parent = nullptr);
|
||||
~EmailTreeModel() override;
|
||||
|
||||
// Basic model interface
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
|
||||
// Email data management
|
||||
void setEmails(const QVector<MailItem> &emails);
|
||||
void clear();
|
||||
|
||||
// Grouping
|
||||
enum GroupMode {
|
||||
NoGrouping = 0,
|
||||
GroupByDate = 1
|
||||
};
|
||||
void setGroupMode(GroupMode mode);
|
||||
GroupMode groupMode() const { return m_groupMode; }
|
||||
|
||||
private:
|
||||
struct TreeItem {
|
||||
enum Type { Group, Mail } type;
|
||||
QString groupName; // for Group type
|
||||
int groupIndex = -1; // for Group type
|
||||
int mailIndex = -1; // for Mail type (index in m_emails)
|
||||
QVector<TreeItem*> children;
|
||||
TreeItem* parent = nullptr;
|
||||
int row = -1;
|
||||
|
||||
TreeItem(Type t, TreeItem* p = nullptr) : type(t), parent(p) {}
|
||||
~TreeItem() { qDeleteAll(children); }
|
||||
};
|
||||
|
||||
GroupMode m_groupMode = NoGrouping;
|
||||
QVector<MailItem> m_emails;
|
||||
TreeItem* m_rootItem = nullptr;
|
||||
|
||||
void setupModelData();
|
||||
void buildGroups();
|
||||
QString groupNameForDate(const QDateTime &date) const;
|
||||
int groupIndexForDate(const QDateTime &date) const;
|
||||
void deleteTree(TreeItem* item);
|
||||
};
|
||||
+56
-23
@@ -20,6 +20,7 @@
|
||||
#include <QFileDialog>
|
||||
#include <QDir>
|
||||
#include <functional>
|
||||
#include <QGraphicsDropShadowEffect>
|
||||
#include "db/dao/mailitemdao.h"
|
||||
|
||||
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
|
||||
@@ -29,7 +30,7 @@ ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
void ReaderView::setupUI() {
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setContentsMargins(12, 0, 12, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// ===== Toolbar (zoom, find, images) =====
|
||||
@@ -182,23 +183,31 @@ void ReaderView::setupUI() {
|
||||
mainLayout->addWidget(m_toolbar);
|
||||
mainLayout->addWidget(m_findBar);
|
||||
|
||||
// ===== Header Section =====
|
||||
m_headerWidget = new QWidget();
|
||||
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
|
||||
QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget);
|
||||
headerLayout->setContentsMargins(16, 12, 16, 12);
|
||||
headerLayout->setSpacing(8);
|
||||
// ===== Subject Section (independent, with shadow) =====
|
||||
QFrame *subjectFrame = new QFrame();
|
||||
subjectFrame->setObjectName("SubjectFrame");
|
||||
subjectFrame->setStyleSheet(
|
||||
"QFrame#SubjectFrame {"
|
||||
" background: white;"
|
||||
" border: none;"
|
||||
" border: 1px solid #e0e0e0;"
|
||||
" border-radius: 10px;"
|
||||
"}"
|
||||
);
|
||||
// Add shadow effect
|
||||
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
|
||||
shadowEffect->setBlurRadius(8);
|
||||
shadowEffect->setOffset(0, 3);
|
||||
shadowEffect->setColor(QColor(0, 0, 0, 40));
|
||||
subjectFrame->setGraphicsEffect(shadowEffect);
|
||||
|
||||
QVBoxLayout *subjectLayout = new QVBoxLayout(subjectFrame);
|
||||
subjectLayout->setContentsMargins(16, 16, 16, 16);
|
||||
subjectLayout->setSpacing(8);
|
||||
|
||||
// Subject row
|
||||
QHBoxLayout *subjectRow = new QHBoxLayout();
|
||||
subjectRow->setSpacing(12);
|
||||
|
||||
m_avatarLabel = new QLabel();
|
||||
m_avatarLabel->setFixedSize(40, 40);
|
||||
m_avatarLabel->setAlignment(Qt::AlignCenter);
|
||||
m_avatarLabel->setStyleSheet("QLabel { background: #1976D2; color: white; border-radius: 20px; font-weight: bold; font-size: 14px; }");
|
||||
subjectRow->addWidget(m_avatarLabel);
|
||||
|
||||
m_subjectLabel = new QLabel();
|
||||
m_subjectLabel->setText("(Sin asunto)");
|
||||
QFont subjectFont = m_subjectLabel->font();
|
||||
@@ -210,12 +219,33 @@ void ReaderView::setupUI() {
|
||||
m_subjectLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
subjectRow->addWidget(m_subjectLabel, 1);
|
||||
|
||||
headerLayout->addLayout(subjectRow);
|
||||
subjectLayout->addLayout(subjectRow);
|
||||
mainLayout->addWidget(subjectFrame);
|
||||
// Add spacing after subject frame so shadow is visible
|
||||
mainLayout->addSpacing(12);
|
||||
|
||||
// From / To / Date row
|
||||
// ===== Header Section =====
|
||||
m_headerWidget = new QWidget();
|
||||
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
|
||||
QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget);
|
||||
headerLayout->setContentsMargins(16, 12, 16, 12);
|
||||
headerLayout->setSpacing(8);
|
||||
|
||||
// Avatar + From / To / Date row
|
||||
QHBoxLayout *metaRow = new QHBoxLayout();
|
||||
metaRow->setSpacing(16);
|
||||
|
||||
// Avatar on the left
|
||||
m_avatarLabel = new QLabel();
|
||||
m_avatarLabel->setFixedSize(40, 40);
|
||||
m_avatarLabel->setAlignment(Qt::AlignCenter);
|
||||
m_avatarLabel->setStyleSheet("QLabel { background: #1976D2; color: white; border-radius: 20px; font-weight: bold; font-size: 14px; }");
|
||||
metaRow->addWidget(m_avatarLabel, 0, Qt::AlignTop);
|
||||
|
||||
// From / To / Date on the right of avatar
|
||||
QVBoxLayout *metaRightLayout = new QVBoxLayout();
|
||||
metaRightLayout->setSpacing(2);
|
||||
|
||||
m_fromLabel = new QLabel();
|
||||
m_fromLabel->setStyleSheet("color: #333; font-size: 13px;");
|
||||
m_fromLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
@@ -223,17 +253,19 @@ void ReaderView::setupUI() {
|
||||
connect(m_fromLabel, &QLabel::linkActivated, [this](const QString &link) {
|
||||
QDesktopServices::openUrl(QUrl(link));
|
||||
});
|
||||
metaRow->addWidget(m_fromLabel, 1);
|
||||
metaRightLayout->addWidget(m_fromLabel);
|
||||
|
||||
m_toLabel = new QLabel();
|
||||
m_toLabel->setStyleSheet("color: #666; font-size: 12px;");
|
||||
m_toLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
m_toLabel->setWordWrap(true);
|
||||
metaRow->addWidget(m_toLabel, 2);
|
||||
metaRightLayout->addWidget(m_toLabel);
|
||||
|
||||
m_dateLabel = new QLabel();
|
||||
m_dateLabel->setStyleSheet("color: #888; font-size: 12px;");
|
||||
metaRow->addWidget(m_dateLabel);
|
||||
metaRightLayout->addWidget(m_dateLabel);
|
||||
|
||||
metaRow->addLayout(metaRightLayout, 1);
|
||||
|
||||
headerLayout->addLayout(metaRow);
|
||||
|
||||
@@ -310,16 +342,17 @@ void ReaderView::setupUI() {
|
||||
actionsLayout->addWidget(m_moreButton);
|
||||
|
||||
headerLayout->addLayout(actionsLayout);
|
||||
|
||||
// ===== Body Viewer (inside header widget) =====
|
||||
setupBodyViewer();
|
||||
headerLayout->addWidget(m_scrollArea, 1);
|
||||
|
||||
mainLayout->addWidget(m_headerWidget);
|
||||
|
||||
// ===== Attachments Area =====
|
||||
setupAttachmentsArea();
|
||||
mainLayout->addWidget(m_attachmentsFrame);
|
||||
|
||||
// ===== Body Viewer =====
|
||||
setupBodyViewer();
|
||||
mainLayout->addWidget(m_scrollArea, 1);
|
||||
|
||||
// Shortcuts
|
||||
QShortcut *findShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_F), this);
|
||||
connect(findShortcut, &QShortcut::activated, [this]() {
|
||||
|
||||
Reference in New Issue
Block a user