diff --git a/src/ui/maillistview.cpp b/src/ui/maillistview.cpp index 6b95dd0..3e4babc 100644 --- a/src/ui/maillistview.cpp +++ b/src/ui/maillistview.cpp @@ -133,23 +133,20 @@ void MailListView::setupUI() this, &MailListView::onDisplayModeChanged); filterLayout->addWidget(m_displayModeCombo); - // Group by combo - m_groupByCombo = new QComboBox(); - m_groupByCombo->addItem("Sin agrupación"); - m_groupByCombo->addItem("Agrupar por fecha"); - m_groupByCombo->addItem("Agrupar por conversación"); - m_groupByCombo->addItem("Agrupar por remitente"); - m_groupByCombo->setFixedWidth(180); - m_groupByCombo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - 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::of(&QComboBox::currentIndexChanged), - this, &MailListView::onGroupByChanged); - filterLayout->addWidget(m_groupByCombo); + // Grouping toggles (Wino-style, orthogonal/independent) + m_groupByDateCheck = new QCheckBox(tr("Por fecha")); + m_groupByDateCheck->setToolTip(tr("Agrupar la lista por fecha (Hoy, Ayer, Semana...); se combina con conversaciones")); + m_groupByDateCheck->setChecked(true); + m_groupByDateCheck->setStyleSheet("QCheckBox { font-size:12px; color:#3a3a3c; }"); + m_groupByThreadCheck = new QCheckBox(tr("Conversaciones")); + m_groupByThreadCheck->setToolTip(tr("Colapsar los correos del mismo hilo en una conversación (independiente de la agrupación por fecha)")); + m_groupByThreadCheck->setChecked(true); + m_groupByThreadCheck->setStyleSheet("QCheckBox { font-size:12px; color:#3a3a3c; }"); + connect(m_groupByDateCheck, &QCheckBox::toggled, this, &MailListView::onGroupingChanged); + connect(m_groupByThreadCheck, &QCheckBox::toggled, this, &MailListView::onGroupingChanged); + filterLayout->addWidget(new QLabel(tr("Agrupar:"))); + filterLayout->addWidget(m_groupByDateCheck); + filterLayout->addWidget(m_groupByThreadCheck); layout->addWidget(filterBar); @@ -244,12 +241,16 @@ void MailListView::setupUI() ); m_treeModel = new EmailTreeModel(this); - // Wino-style default: group inbox by conversation (thread) - m_treeModel->setGroupMode(EmailTreeModel::GroupByThread); - // Reflect the default in the group combo (model already set, block to avoid re-trigger) - m_groupByCombo->blockSignals(true); - m_groupByCombo->setCurrentIndex(2); // "Agrupar por conversación" - m_groupByCombo->blockSignals(false); + // Wino-style default: group by date AND collapse conversations (orthogonal). + m_treeModel->setGroupByDate(true); + m_treeModel->setGroupByThread(true); + // Reflect defaults in the toggles (model already set, block to avoid re-trigger) + m_groupByDateCheck->blockSignals(true); + m_groupByDateCheck->setChecked(true); + m_groupByDateCheck->blockSignals(false); + m_groupByThreadCheck->blockSignals(true); + m_groupByThreadCheck->setChecked(true); + m_groupByThreadCheck->blockSignals(false); m_proxyModel = new QSortFilterProxyModel(this); m_proxyModel->setSourceModel(m_treeModel); m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); @@ -434,7 +435,8 @@ void MailListView::updateFilterBarVisibility(int viewportWidth) bool showDisplayMode = viewportWidth > 600; bool showPivot = viewportWidth > 500; - m_groupByCombo->setVisible(showGroupBy); + m_groupByDateCheck->setVisible(showGroupBy); + m_groupByThreadCheck->setVisible(showGroupBy); m_displayModeCombo->setVisible(showDisplayMode); m_pivotCombo->setVisible(showPivot && m_pivotCombo->isVisible()); // respect folder-type visibility } @@ -532,23 +534,16 @@ void MailListView::onFilterChanged() m_sourceModel->setShowPinnedOnly(m_pinnedOnlyCheck->isChecked()); } -void MailListView::onGroupByChanged(int index) +void MailListView::onGroupingChanged() { if (!m_treeModel) return; - switch (index) { - case 0: // Sin agrupación - m_treeModel->setGroupMode(EmailTreeModel::NoGrouping); - break; - case 1: // Agrupar por fecha - m_treeModel->setGroupMode(EmailTreeModel::GroupByDate); - break; - case 2: // Agrupar por conversación/hilo - m_treeModel->setGroupMode(EmailTreeModel::GroupByThread); - break; - case 3: // Agrupar por remitente - m_treeModel->setGroupMode(EmailTreeModel::GroupBySender); - break; - } + // Orthogonal toggles: date grouping and conversation collapse combine freely + // (Wino-style). Sender grouping is offered separately when date is off. + bool dateOn = m_groupByDateCheck ? m_groupByDateCheck->isChecked() : true; + bool threadOn = m_groupByThreadCheck ? m_groupByThreadCheck->isChecked() : true; + + m_treeModel->setGroupByDate(dateOn); + m_treeModel->setGroupByThread(threadOn); if (m_treeView) m_treeView->expandAll(); } diff --git a/src/ui/maillistview.h b/src/ui/maillistview.h index 4d8bb61..5227417 100644 --- a/src/ui/maillistview.h +++ b/src/ui/maillistview.h @@ -61,7 +61,7 @@ private slots: void onRowSelected(const QModelIndex ¤t, const QModelIndex &previous); void onSearchTextChanged(const QString &text); void onFilterChanged(); - void onGroupByChanged(int index); + void onGroupingChanged(); void onCompactFlagRequested(int mailId); void onCompactDeleteRequested(int mailId); void onCompactCategoryRequested(int mailId); @@ -119,7 +119,8 @@ private: QCheckBox *m_selectAllCheck; // Select all checkbox in action bar QWidget *m_actionBar; // Action bar widget QToolButton *m_onlineSearchBtn; // Online search button - QComboBox *m_groupByCombo; + QCheckBox *m_groupByDateCheck; // independent: group by date (Hoy/Ayer/...) + QCheckBox *m_groupByThreadCheck; // independent: collapse conversations QComboBox *m_displayModeCombo; QComboBox *m_pivotCombo; // Focused/Other pivot CompactMailDelegate *m_compactDelegate = nullptr; diff --git a/src/ui/models/EmailTreeModel.cpp b/src/ui/models/EmailTreeModel.cpp index 69cd1c9..ab1759d 100644 --- a/src/ui/models/EmailTreeModel.cpp +++ b/src/ui/models/EmailTreeModel.cpp @@ -288,6 +288,40 @@ void EmailTreeModel::setGroupMode(GroupMode mode) endResetModel(); } +// Orthogonal grouping toggles (Wino-style: each on/off independently). +void EmailTreeModel::setGroupByDate(bool on) +{ + if (m_groupByDate == on) return; + beginResetModel(); + m_groupByDate = on; + qDeleteAll(m_rootItem->children); + m_rootItem->children.clear(); + setupModelData(); + endResetModel(); +} + +void EmailTreeModel::setGroupByThread(bool on) +{ + if (m_groupByThread == on) return; + beginResetModel(); + m_groupByThread = on; + qDeleteAll(m_rootItem->children); + m_rootItem->children.clear(); + setupModelData(); + endResetModel(); +} + +void EmailTreeModel::setGroupBySender(bool on) +{ + if (m_groupBySender == on) return; + beginResetModel(); + m_groupBySender = on; + qDeleteAll(m_rootItem->children); + m_rootItem->children.clear(); + setupModelData(); + endResetModel(); +} + void EmailTreeModel::setThreadExpanded(const QString& threadId, bool expanded) { if (expanded) { @@ -317,7 +351,19 @@ bool EmailTreeModel::isThreadExpanded(const QString& threadId) const void EmailTreeModel::setupModelData() { - if (m_groupMode == NoGrouping) { + bool dateBase = m_groupByDate || m_groupMode == GroupByDate || m_groupMode == NoGrouping; + bool threadOn = m_groupByThread || m_groupMode == GroupByThread; + bool senderOn = m_groupBySender || m_groupMode == GroupBySender; + + // Wino-style: date grouping is the base kind, conversation (thread) collapse + // is an independent, orthogonal flag applied inside whatever grouping is active. + if (senderOn && !dateBase && !threadOn) { + buildSenderGroups(); + } else if (dateBase) { + buildGroups(); + } else if (senderOn) { + buildSenderGroups(); + } else { // Flat list - all mails directly under root for (int i = 0; i < m_emails.size(); ++i) { if (i < 0 || i >= m_emails.size()) continue; @@ -326,15 +372,91 @@ void EmailTreeModel::setupModelData() mailItem->row = m_rootItem->children.size(); m_rootItem->children.append(mailItem); } - } else if (m_groupMode == GroupByDate) { - // Grouped by date - buildGroups(); - } else if (m_groupMode == GroupByThread) { - // Grouped by thread/conversation - buildThreads(); - } else if (m_groupMode == GroupBySender) { - // Grouped by sender - buildSenderGroups(); + } + + // Orthogonal: if conversation grouping is on, collapse threads within each + // top-level group (or the root if flat). + if (threadOn) + collapseThreads(m_rootItem); +} + +/// Helper: add a Mail item under the given parent. +void EmailTreeModel::addMailItem(TreeItem* parent, int mailIndex) +{ + if (!parent || mailIndex < 0 || mailIndex >= m_emails.size()) return; + TreeItem* mailItem = new TreeItem(TreeItem::Mail, parent); + mailItem->mailIndex = mailIndex; + mailItem->row = parent->children.size(); + parent->children.append(mailItem); +} + +/// Collapse mails into conversation (thread) nodes under `parent`. +/// Single-message threads stay as plain mail rows (no expander), following Wino. +void EmailTreeModel::collapseThreads(TreeItem* parent) +{ + if (!parent) return; + + // Collect mail indices in present (visible) order; recurse into subgroups first. + QVector flatIndices; + for (TreeItem* item : parent->children) { + if (item->type == TreeItem::Group) { + collapseThreads(item); // recurse into date/sender groups + } else if (item->type == TreeItem::Mail && item->mailIndex >= 0) { + flatIndices.append(item->mailIndex); + } + } + + if (flatIndices.isEmpty()) return; + + // Group by thread id (preserving encounter order). + QVector ordered; + QSet seen; + QMap> threadMap; + for (int idx : flatIndices) { + const MailItem& mail = m_emails[idx]; + QString tid = extractThreadId(mail); + if (tid.isEmpty()) tid = ThreadUtils::generateThreadId(mail.messageId(), mail.subject(), mail.sender()); + threadMap[tid].append(idx); + if (!seen.contains(tid)) { seen.insert(tid); ordered.append(idx); } + } + + // Rebuild this level from scratch (fresh nodes) so qDeleteAll below is safe. + QVector newChildren; + for (int leadIdx : ordered) { + const MailItem& lead = m_emails[leadIdx]; + QString tid = extractThreadId(lead); + if (tid.isEmpty()) tid = ThreadUtils::generateThreadId(lead.messageId(), lead.subject(), lead.sender()); + const QVector& members = threadMap[tid]; + + if (members.size() <= 1) { + // Single message: plain mail row (no expander). + TreeItem* mi = new TreeItem(TreeItem::Mail, parent); + mi->mailIndex = members.first(); + mi->row = newChildren.size(); + newChildren.append(mi); + } else { + // Multi-message thread: a collapsible conversation node with children. + TreeItem* ti = new TreeItem(TreeItem::Thread, parent); + ti->threadId = tid; + ti->groupIndex = newChildren.size(); + ti->expanded = m_expandedThreads.contains(tid); + for (int mIdx : members) { + TreeItem* mi = new TreeItem(TreeItem::Mail, ti); + mi->mailIndex = mIdx; + mi->row = ti->children.size(); + ti->children.append(mi); + } + ti->row = newChildren.size(); + newChildren.append(ti); + } + } + + // Replace existing children entirely. + qDeleteAll(parent->children); + parent->children = newChildren; + for (int i = 0; i < newChildren.size(); ++i) { + newChildren[i]->parent = parent; + newChildren[i]->row = i; } } diff --git a/src/ui/models/EmailTreeModel.h b/src/ui/models/EmailTreeModel.h index 8c70483..9cd41a1 100644 --- a/src/ui/models/EmailTreeModel.h +++ b/src/ui/models/EmailTreeModel.h @@ -47,7 +47,7 @@ public: void setEmails(const QVector &emails); void clear(); - // Grouping + // Grouping (Wino-style: date grouping is orthogonal to conversation grouping). enum GroupMode { NoGrouping = 0, GroupByDate = 1, @@ -57,6 +57,16 @@ public: void setGroupMode(GroupMode mode); GroupMode groupMode() const { return m_groupMode; } + /// Conversation grouping is an independent, combinable flag (like Wino): + /// when TRUE, mails sharing a thread are collapsed under a conversation node; + /// when combined with date grouping, threads nest under the date groups. + void setGroupByThread(bool on); + bool groupByThread() const { return m_groupByThread; } + void setGroupByDate(bool on); + bool groupByDate() const { return m_groupByDate; } + void setGroupBySender(bool on); + bool groupBySender() const { return m_groupBySender; } + // Thread expansion void setThreadExpanded(const QString& threadId, bool expanded); bool isThreadExpanded(const QString& threadId) const; @@ -77,7 +87,10 @@ private: ~TreeItem() { qDeleteAll(children); } }; - GroupMode m_groupMode = NoGrouping; + GroupMode m_groupMode = GroupByDate; // default: date-based (Wino-style) + bool m_groupByDate = false; // orthogonal flags override groupMode + bool m_groupByThread = false; + bool m_groupBySender = false; QVector m_emails; TreeItem* m_rootItem = nullptr; QSet m_expandedThreads; @@ -91,4 +104,7 @@ private: void deleteTree(TreeItem* item); void buildThreadTree(); QString extractThreadId(const MailItem& mail) const; + // Helper + orthogonal thread-collapse (Wino-style conversation grouping) + void addMailItem(TreeItem* parent, int mailIndex); + void collapseThreads(TreeItem* parent); }; \ No newline at end of file