feat: Multi-selection + action bar in MailListView

- MailListView: ExtendedSelection mode for both tree and list views
- Action bar (blue bar) appears when items selected
- Select all checkbox + counter label
- Batch actions: Flag, Mark unread, Move (TODO), Delete, Exit selection
- Signals: selectionChanged, batchFlagRequested, batchDeleteRequested, batchMarkUnreadRequested, batchMoveRequested
- enterMultiSelectMode()/exitMultiSelectMode() state management
- Works in both normal and compact views
This commit is contained in:
2026-08-26 02:38:33 +02:00
parent 3846bd4e9d
commit 43b828fe87
11 changed files with 11023 additions and 10653 deletions
+235 -52
View File
@@ -15,9 +15,11 @@
#include <QListView>
#include <QStyledItemDelegate>
#include <QScrollBar>
#include <QToolButton>
#include <QMenu>
#include "ui/delegates/CompactMailDelegate.h"
MailListView::MailListView(QWidget *parent) : QWidget(parent), m_sourceModel(nullptr)
MailListView::MailListView(QWidget *parent) : QWidget(parent), m_sourceModel(nullptr), m_multiSelectMode(false)
{
setupUI();
}
@@ -110,6 +112,67 @@ void MailListView::setupUI()
layout->addWidget(filterBar);
// Action bar (hidden by default, shown when multi-select mode active)
m_actionBar = new QWidget();
m_actionBar->setFixedHeight(44);
m_actionBar->setStyleSheet("background-color: #e3f2fd; border-bottom: 1px solid #bbdefb;");
m_actionBar->setVisible(false);
QHBoxLayout *actionBarLayout = new QHBoxLayout(m_actionBar);
actionBarLayout->setContentsMargins(12, 4, 12, 4);
actionBarLayout->setSpacing(8);
m_selectAllCheck = new QCheckBox("Seleccionar todo");
m_selectAllCheck->setStyleSheet("QCheckBox { font-size: 13px; color: #1d1d1f; }");
connect(m_selectAllCheck, &QCheckBox::toggled, this, &MailListView::onSelectAllToggled);
actionBarLayout->addWidget(m_selectAllCheck);
QLabel *selectionLabel = new QLabel("0 elementos seleccionados");
selectionLabel->setStyleSheet("font-size: 13px; color: #1d1d1f; font-weight: 500;");
selectionLabel->setObjectName("selectionLabel");
actionBarLayout->addWidget(selectionLabel);
actionBarLayout->addStretch();
// Action buttons
auto createActionButton = [&](const QString& text, std::function<void()> slot) -> QToolButton* {
QToolButton *btn = new QToolButton();
btn->setText(text);
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
btn->setStyleSheet(
"QToolButton { background: white; border: 1px solid #d1d1d6; border-radius: 6px; "
"padding: 6px 12px; font-size: 12px; color: #1d1d1f; }"
"QToolButton:hover { background: #f0f0f5; border-color: #bdbdbd; }"
"QToolButton:pressed { background: #e0e0e5; }"
);
connect(btn, &QToolButton::clicked, this, slot);
return btn;
};
QToolButton *btnFlag = createActionButton("★ Marcar", [this]() { onBatchFlagClicked(); });
actionBarLayout->addWidget(btnFlag);
QToolButton *btnUnread = createActionButton("✎ No leído", [this]() { onBatchMarkUnreadClicked(); });
actionBarLayout->addWidget(btnUnread);
QToolButton *btnMove = createActionButton("↗ Mover", [this]() { onBatchMoveClicked(); });
actionBarLayout->addWidget(btnMove);
QToolButton *btnDelete = createActionButton("🗑 Borrar", [this]() { onBatchDeleteClicked(); });
btnDelete->setStyleSheet(btnDelete->styleSheet() + "QToolButton { color: #ff3b30; border-color: #ff3b30; }");
actionBarLayout->addWidget(btnDelete);
QToolButton *btnExit = new QToolButton();
btnExit->setText("✕ Salir selección");
btnExit->setToolButtonStyle(Qt::ToolButtonTextOnly);
btnExit->setStyleSheet(
"QToolButton { padding: 6px 12px; font-size: 12px; color: #0071e3; }"
"QToolButton:hover { background: #e8f0fe; border-radius: 6px; }"
);
connect(btnExit, &QToolButton::clicked, this, [this]() { exitMultiSelectMode(); });
actionBarLayout->addWidget(btnExit);
layout->insertWidget(1, m_actionBar); // Insert after search bar
// View Stack (TreeView for normal, ListView for compact)
m_viewStack = new QStackedWidget();
layout->addWidget(m_viewStack, 1);
@@ -117,7 +180,7 @@ void MailListView::setupUI()
// --- Normal Tree View ---
m_treeView = new QTreeView();
m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_treeView->setSelectionMode(QAbstractItemView::SingleSelection);
m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection); // Multi-selection
m_treeView->setAlternatingRowColors(true);
m_treeView->header()->setStretchLastSection(false);
m_treeView->header()->setSectionsClickable(true);
@@ -146,11 +209,12 @@ void MailListView::setupUI()
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->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &MailListView::onSelectionChanged);
connect(m_treeView, &QTreeView::doubleClicked, this, [this](const QModelIndex &index) {
if (!index.isValid()) return;
QModelIndex proxyIdx = m_proxyModel->mapToSource(index);
// Check if it's a thread node - toggle expansion
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::IsThreadRole).toBool()) {
QString threadId = proxyIdx.data(EmailTreeModel::ThreadIdRole).toString();
@@ -160,7 +224,7 @@ void MailListView::setupUI()
return;
}
}
// Otherwise open mail
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::MailIdRole).isValid()) {
int mailId = proxyIdx.data(EmailTreeModel::MailIdRole).toInt();
@@ -172,7 +236,7 @@ void MailListView::setupUI()
// --- Compact List View ---
m_listView = new QListView();
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection); // Multi-selection
m_listView->setUniformItemSizes(true);
m_listView->setFrameShape(QFrame::NoFrame);
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -197,11 +261,20 @@ void MailListView::setupUI()
this, &MailListView::onCompactMoreRequested);
connect(m_compactDelegate, &CompactMailDelegate::mailClicked,
this, &MailListView::onCompactMailClicked);
connect(m_compactDelegate, &CompactMailDelegate::replyRequested,
this, &MailListView::onCompactReplyRequested);
connect(m_compactDelegate, &CompactMailDelegate::forwardRequested,
this, &MailListView::onCompactForwardRequested);
connect(m_compactDelegate, &CompactMailDelegate::markUnreadRequested,
this, &MailListView::onCompactMarkUnreadRequested);
// Same proxy model for list view (single column)
m_listView->setModel(m_proxyModel);
m_listView->setModelColumn(0); // Use first column (subject)
connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &MailListView::onSelectionChanged);
// Add both views to stack
m_viewStack->addWidget(m_treeView); // index 0 = normal
m_viewStack->addWidget(m_listView); // index 1 = compact
@@ -271,13 +344,82 @@ void MailListView::updateViewMode()
}
}
void MailListView::onRowSelected(const QModelIndex &index)
QVector<int> MailListView::getSelectedMailIds() const
{
if (!index.isValid()) return;
QModelIndex proxyIdx = m_proxyModel->mapToSource(index);
if (proxyIdx.isValid() && proxyIdx.data(EmailTreeModel::MailIdRole).isValid()) {
int mailId = proxyIdx.data(EmailTreeModel::MailIdRole).toInt();
emit emailSelected(mailId);
QVector<int> mailIds;
QAbstractItemView *currentView = qobject_cast<QAbstractItemView*>(m_viewStack->currentWidget());
QItemSelectionModel *selectionModel = currentView->selectionModel();
if (!selectionModel) return mailIds;
QModelIndexList selectedIndexes = selectionModel->selectedRows();
for (const QModelIndex &proxyIdx : selectedIndexes) {
QModelIndex sourceIdx = m_proxyModel->mapToSource(proxyIdx);
if (sourceIdx.isValid() && sourceIdx.data(EmailTreeModel::MailIdRole).isValid()) {
int mailId = sourceIdx.data(EmailTreeModel::MailIdRole).toInt();
mailIds.append(mailId);
}
}
return mailIds;
}
void MailListView::updateActionBar()
{
QLabel *label = m_actionBar->findChild<QLabel*>("selectionLabel");
if (label) {
label->setText(QString("%1 elementos seleccionados").arg(m_selectedMailIds.size()));
}
m_selectAllCheck->blockSignals(true);
m_selectAllCheck->setChecked(m_selectedMailIds.size() > 0);
m_selectAllCheck->blockSignals(false);
}
void MailListView::onSelectionChanged()
{
m_selectedMailIds = getSelectedMailIds();
bool hasSelection = !m_selectedMailIds.isEmpty();
if (hasSelection && !m_multiSelectMode) {
enterMultiSelectMode();
} else if (!hasSelection && m_multiSelectMode) {
exitMultiSelectMode();
}
updateActionBar();
emit selectionChanged(m_selectedMailIds);
}
void MailListView::enterMultiSelectMode()
{
m_multiSelectMode = true;
m_actionBar->setVisible(true);
updateActionBar();
}
void MailListView::exitMultiSelectMode()
{
m_multiSelectMode = false;
m_selectedMailIds.clear();
m_actionBar->setVisible(false);
// Clear selections
m_treeView->clearSelection();
m_listView->clearSelection();
m_selectAllCheck->blockSignals(true);
m_selectAllCheck->setChecked(false);
m_selectAllCheck->blockSignals(false);
updateActionBar();
emit selectionChanged(QVector<int>());
}
void MailListView::onSelectAllToggled(bool checked)
{
QAbstractItemView *currentView = qobject_cast<QAbstractItemView*>(m_viewStack->currentWidget());
if (checked) {
currentView->selectAll();
} else {
currentView->clearSelection();
}
}
@@ -314,46 +456,6 @@ void MailListView::onGroupByChanged(int index)
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);
}
void MailListView::onCompactReplyRequested(int mailId)
{
emit replyRequested(mailId);
}
void MailListView::onCompactForwardRequested(int mailId)
{
emit forwardRequested(mailId);
}
void MailListView::onCompactMarkUnreadRequested(int mailId)
{
emit markUnreadRequested(mailId);
}
void MailListView::onDisplayModeChanged(int index)
{
CompactMailDelegate::DisplayMode mode = static_cast<CompactMailDelegate::DisplayMode>(index);
@@ -398,4 +500,85 @@ void MailListView::onRowsInserted(const QModelIndex &parent, int first, int last
m_treeModel->setEmails(emails);
m_treeView->expandAll();
}
}
void MailListView::onRowSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
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::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);
}
void MailListView::onCompactReplyRequested(int mailId)
{
emit replyRequested(mailId);
}
void MailListView::onCompactForwardRequested(int mailId)
{
emit forwardRequested(mailId);
}
void MailListView::onCompactMarkUnreadRequested(int mailId)
{
emit markUnreadRequested(mailId);
}
// Batch action slots
void MailListView::onBatchFlagClicked()
{
if (m_selectedMailIds.isEmpty()) return;
emit batchFlagRequested(m_selectedMailIds, true);
exitMultiSelectMode();
}
void MailListView::onBatchMarkUnreadClicked()
{
if (m_selectedMailIds.isEmpty()) return;
emit batchMarkUnreadRequested(m_selectedMailIds);
exitMultiSelectMode();
}
void MailListView::onBatchMoveClicked()
{
if (m_selectedMailIds.isEmpty()) return;
// TODO: Show folder picker dialog
// For now, just emit signal
// emit batchMoveRequested(m_selectedMailIds, targetFolderId);
exitMultiSelectMode();
}
void MailListView::onBatchDeleteClicked()
{
if (m_selectedMailIds.isEmpty()) return;
emit batchDeleteRequested(m_selectedMailIds);
exitMultiSelectMode();
}
+20
View File
@@ -41,6 +41,12 @@ signals:
void replyRequested(int mailId);
void forwardRequested(int mailId);
void markUnreadRequested(int mailId);
// Multi-selection signals
void selectionChanged(const QVector<int>& selectedMailIds);
void batchFlagRequested(const QVector<int>& mailIds, bool flagged);
void batchDeleteRequested(const QVector<int>& mailIds);
void batchMarkUnreadRequested(const QVector<int>& mailIds);
void batchMoveRequested(const QVector<int>& mailIds, int targetFolderId);
private slots:
void onRowSelected(const QModelIndex &index);
@@ -60,6 +66,14 @@ private slots:
void onTreeViewScrolled(int value);
void onListViewScrolled(int value);
void onRowsInserted(const QModelIndex &parent, int first, int last);
void onSelectionChanged();
void onSelectAllToggled(bool checked);
void onBatchFlagClicked();
void onBatchMarkUnreadClicked();
void onBatchMoveClicked();
void onBatchDeleteClicked();
void enterMultiSelectMode();
void exitMultiSelectMode();
protected:
void resizeEvent(QResizeEvent *event) override;
@@ -68,6 +82,8 @@ private:
void setupUI();
void refreshViews();
void updateViewMode();
void updateActionBar();
QVector<int> getSelectedMailIds() const;
QTreeView *m_treeView;
QListView *m_listView;
@@ -81,9 +97,13 @@ private:
QCheckBox *m_flaggedOnlyCheck;
QCheckBox *m_hasAttachmentsCheck;
QCheckBox *m_pinnedOnlyCheck;
QCheckBox *m_selectAllCheck; // Select all checkbox in action bar
QWidget *m_actionBar; // Action bar widget
QComboBox *m_groupByCombo;
QComboBox *m_displayModeCombo;
CompactMailDelegate *m_compactDelegate = nullptr;
bool m_compactMode = false;
int m_compactThreshold = 650;
bool m_multiSelectMode = false;
QVector<int> m_selectedMailIds;
};