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:
+235
-52
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user