feat: rework main window - always-visible left navigator panel, remove sidebar

- Left panel is now always visible and its content changes per section:
  * Emails -> folder + category tree
  * Contactos -> compact contact list (aggregated from local mails, by frequency)
  * Calendario -> mini QCalendarWidget
- Sidebar removed; the three navigation buttons (Emails/Contactos/Calendario)
  sit at the bottom of the left panel instead (no Compose, no Settings, no
  'new mail' button)
- Mail list + reader splitter now only holds list | viewer (folders moved out)
- switchToPage syncs the navigator stack + highlighted button

Verified: compiles and runs clean in Linux.
This commit is contained in:
2026-09-05 11:19:20 +02:00
parent 64d6bc07c9
commit 30a425cf8a
2 changed files with 218 additions and 69 deletions
+205 -67
View File
@@ -22,6 +22,11 @@
#include <QFrame> #include <QFrame>
#include <QDateTime> #include <QDateTime>
#include <QLabel> #include <QLabel>
#include <QHash>
#include <QRegularExpression>
#include <QListWidget>
#include <QCalendarWidget>
#include <algorithm>
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#include <windows.h> #include <windows.h>
#endif #endif
@@ -57,12 +62,15 @@ void MainMainWindow::setupUI()
{ {
qDebug() << "[MainMainWindow::setupUI] Start"; qDebug() << "[MainMainWindow::setupUI] Start";
// === Global stylesheet === // === Global stylesheet ===
this->setStyleSheet( /*this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }" "QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }" "QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
, k
);*/
qDebug() << "[MainMainWindow::setupUI] Stylesheet set"; qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
createTitleBar(); createTitleBar();
@@ -71,23 +79,23 @@ void MainMainWindow::setupUI()
// === Central widget === // === Central widget ===
QWidget *central = new QWidget(); QWidget *central = new QWidget();
central->setObjectName("CentralWidget"); central->setObjectName("CentralWidget");
central->setStyleSheet( /*central->setStyleSheet(
"QWidget#CentralWidget {" "QWidget#CentralWidget {"
" background-color: #f5f5f7;" " background-color: #f5f5f7;"
" border-bottom-left-radius: 8px;" " border-bottom-left-radius: 8px;"
" border-bottom-right-radius: 8px;" " border-bottom-right-radius: 8px;"
"}" "}"
); );*/
// Install event filter to handle resize from window edges // Install event filter to handle resize from window edges
central->installEventFilter(this); central->installEventFilter(this);
QHBoxLayout *centralLayout = new QHBoxLayout(central); QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0); centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0); centralLayout->setSpacing(0);
// === Sidebar === // === Left navigator panel (always visible) ===
setupSidebar(); setupNavigator();
qDebug() << "[MainMainWindow::setupUI] Sidebar created"; qDebug() << "[MainMainWindow::setupUI] Navigator created";
centralLayout->addWidget(m_sidebar); centralLayout->addWidget(m_navigatorPanel);
// === Separator line === // === Separator line ===
QFrame *separator = new QFrame(); QFrame *separator = new QFrame();
@@ -187,28 +195,120 @@ void MainMainWindow::setupUI()
qDebug() << "[MainMainWindow::setupUI] Done"; qDebug() << "[MainMainWindow::setupUI] Done";
} }
void MainMainWindow::setupSidebar() void MainMainWindow::setupNavigator()
{ {
m_sidebar = new QListWidget(); // Always-visible left panel: its content (navigator) changes per section,
m_sidebar->setFixedWidth(100); // and the three section buttons sit at the bottom of the panel.
m_sidebar->setIconSize(QSize(24, 24)); m_navigatorPanel = new QWidget();
m_sidebar->setSpacing(4); m_navigatorPanel->setObjectName("NavigatorPanel");
m_sidebar->setFrameShape(QFrame::NoFrame); m_navigatorPanel->setFixedWidth(240);
m_sidebar->setStyleSheet( m_navigatorPanel->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" "QWidget#NavigatorPanel { background-color: #f7f7f9; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
); );
QVBoxLayout *panelLay = new QVBoxLayout(m_navigatorPanel);
panelLay->setContentsMargins(0, 0, 0, 0);
panelLay->setSpacing(0);
m_sidebar->addItem("📧\nMail"); // The three navigator pages.
m_sidebar->addItem("✏️\nCompose"); m_navStack = new QStackedWidget();
m_sidebar->addItem("⚙️\nSettings"); m_navStack->setStyleSheet("background: transparent;");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0); // Page 0: folder + category navigator (Emails). Built now; the folder/category
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); // trees are wired to models in connectModels().
m_folderNav = new QWidget();
m_folderNav->setObjectName("FolderNav");
QVBoxLayout *folderNavLay = new QVBoxLayout(m_folderNav);
folderNavLay->setContentsMargins(4, 8, 4, 8);
folderNavLay->setSpacing(8);
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(150);
m_folderTree->setMaximumWidth(300);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderTree->setItemDelegate(new FolderTreeDelegate(this));
folderNavLay->addWidget(m_folderTree, 3);
m_categoryTree = new CategoryTreeWidget();
m_categoryTree->setMinimumHeight(120);
m_categoryTree->setMaximumHeight(320);
connect(m_categoryTree, &CategoryTreeWidget::categorySelected, this, &MainMainWindow::onCategorySelected);
connect(m_categoryTree, &CategoryTreeWidget::categoryDoubleClicked, this, &MainMainWindow::onCategoryDoubleClicked);
connect(m_categoryTree, &CategoryTreeWidget::assignCategoryRequested, this, &MainMainWindow::onAssignCategoryRequested);
connect(m_categoryTree, &CategoryTreeWidget::categoryCreated, this, &MainMainWindow::onCategoryCreated);
connect(m_categoryTree, &CategoryTreeWidget::categoryEdited, this, &MainMainWindow::onCategoryEdited);
connect(m_categoryTree, &CategoryTreeWidget::categoryDeleted, this, &MainMainWindow::onCategoryDeleted);
folderNavLay->addWidget(m_categoryTree, 2);
m_navStack->addWidget(m_folderNav);
// Page 1: compact contact list navigator.
m_contactsNav = new QWidget();
m_contactsNav->setObjectName("ContactsNav");
QVBoxLayout *contactsNavLay = new QVBoxLayout(m_contactsNav);
contactsNavLay->setContentsMargins(4, 8, 4, 8);
QLabel *contactsTitle = new QLabel("Contactos");
contactsTitle->setStyleSheet("font-weight:700; font-size:13px; color:#1d1d1f; padding:4px;");
contactsNavLay->addWidget(contactsTitle);
m_contactsNavList = new QListWidget();
m_contactsNavList->setStyleSheet(
"QListWidget { background:#ffffff; border:1px solid #e0e0e5; border-radius:6px; font-size:12px; }"
"QListWidget::item { padding:6px 8px; border-bottom:1px solid #f0f0f3; }"
"QListWidget::item:selected { background:#e8f1fd; color:#1c1c1e; }"
);
contactsNavLay->addWidget(m_contactsNavList);
m_navStack->addWidget(m_contactsNav);
// Page 2: mini calendar navigator.
m_calendarNav = new QWidget();
m_calendarNav->setObjectName("CalendarNav");
QVBoxLayout *calendarNavLay = new QVBoxLayout(m_calendarNav);
calendarNavLay->setContentsMargins(4, 8, 4, 8);
m_calendarNavWidget = new QCalendarWidget();
m_calendarNavWidget->setStyleSheet(
"QCalendarWidget { background:#ffffff; border:1px solid #e0e0e5; border-radius:6px; }"
);
calendarNavLay->addWidget(m_calendarNavWidget);
m_navStack->addWidget(m_calendarNav);
panelLay->addWidget(m_navStack, 1);
// Bottom navigation buttons: only Emails, Contacts, Calendar (no Compose, no Settings).
QWidget *navBtns = new QWidget();
navBtns->setObjectName("NavButtons");
QHBoxLayout *btnLay = new QHBoxLayout(navBtns);
btnLay->setContentsMargins(6, 8, 6, 8);
btnLay->setSpacing(4);
auto makeNavBtn = [&](const QString &text, QToolButton **out) {
QToolButton *b = new QToolButton();
b->setText(text);
b->setToolButtonStyle(Qt::ToolButtonTextOnly);
b->setCheckable(true);
b->setAutoExclusive(true);
b->setStyleSheet(
"QToolButton { border:none; border-radius:6px; padding:8px 4px; font-size:11px; color:#3a3a3c; }"
"QToolButton:hover { background:#e0e4ea; }"
"QToolButton:checked { background:#1976D2; color:#ffffff; font-weight:600; }"
);
if (out) *out = b;
return b;
};
m_navMailBtn = makeNavBtn("📧 Emails", nullptr);
m_navContactsBtn = makeNavBtn("👥 Contactos", nullptr);
m_navCalendarBtn = makeNavBtn("📅 Calendario", nullptr);
btnLay->addWidget(m_navMailBtn, 1);
btnLay->addWidget(m_navContactsBtn, 1);
btnLay->addWidget(m_navCalendarBtn, 1);
panelLay->addWidget(navBtns);
connect(m_navMailBtn, &QToolButton::clicked, this, [this]() { switchToPage(PageMail); });
connect(m_navContactsBtn, &QToolButton::clicked, this, [this]() { switchToPage(PageContacts); });
connect(m_navCalendarBtn, &QToolButton::clicked, this, [this]() { switchToPage(PageCalendar); });
m_navMailBtn->setChecked(true);
m_navStack->setCurrentIndex(0);
} }
void MainMainWindow::setupMailPage() void MainMainWindow::setupMailPage()
@@ -217,45 +317,11 @@ void MainMainWindow::setupMailPage()
mailLayout->setContentsMargins(0, 0, 0, 0); mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0); mailLayout->setSpacing(0);
// Main splitter with 3 panes: folders | categories | mail list | viewer // Main splitter: mail list | viewer (folders/categories now live in the
// always-visible left navigator panel built by setupNavigator()).
m_folderSplitter = new QSplitter(Qt::Horizontal); m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1); m_folderSplitter->setHandleWidth(1);
// Left pane: Folder tree + Category tree (vertical splitter)
QSplitter* leftSplitter = new QSplitter(Qt::Vertical);
leftSplitter->setHandleWidth(1);
leftSplitter->setChildrenCollapsible(false);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(160); // Reduced from 220
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderTree->setItemDelegate(new FolderTreeDelegate(this)); // Custom delegate with unread count badges
leftSplitter->addWidget(m_folderTree);
// Category tree
m_categoryTree = new CategoryTreeWidget();
m_categoryTree->setMinimumHeight(200);
m_categoryTree->setMaximumHeight(400);
connect(m_categoryTree, &CategoryTreeWidget::categorySelected, this, &MainMainWindow::onCategorySelected);
connect(m_categoryTree, &CategoryTreeWidget::categoryDoubleClicked, this, &MainMainWindow::onCategoryDoubleClicked);
connect(m_categoryTree, &CategoryTreeWidget::assignCategoryRequested, this, &MainMainWindow::onAssignCategoryRequested);
connect(m_categoryTree, &CategoryTreeWidget::categoryCreated, this, &MainMainWindow::onCategoryCreated);
connect(m_categoryTree, &CategoryTreeWidget::categoryEdited, this, &MainMainWindow::onCategoryEdited);
connect(m_categoryTree, &CategoryTreeWidget::categoryDeleted, this, &MainMainWindow::onCategoryDeleted);
leftSplitter->addWidget(m_categoryTree);
// Set initial sizes for left splitter: folder tree gets 60%, categories 40%
leftSplitter->setSizes({400, 250});
leftSplitter->setStretchFactor(0, 3);
leftSplitter->setStretchFactor(1, 2);
m_folderSplitter->addWidget(leftSplitter);
// Mail list (QTableView with sorting) // Mail list (QTableView with sorting)
m_mailListView = new MailListView(); m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
@@ -331,7 +397,7 @@ void MainMainWindow::setupMailPage()
m_folderSplitter->addWidget(m_viewerStack); m_folderSplitter->addWidget(m_viewerStack);
// Default sizes: left pane (folders+categories) 300, list 380, viewer flex // Default sizes: left pane (folders+categories) 300, list 380, viewer flex
m_folderSplitter->setSizes({300, 380, 600}); m_folderSplitter->setSizes({420, 600});
// Allow each pane to shrink further // Allow each pane to shrink further
m_folderSplitter->setHandleWidth(1); m_folderSplitter->setHandleWidth(1);
@@ -397,11 +463,83 @@ void MainMainWindow::onNavChanged(int index)
void MainMainWindow::switchToPage(int pageIndex) void MainMainWindow::switchToPage(int pageIndex)
{ {
m_stack->setCurrentIndex(pageIndex); m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page // Sync the left navigator panel: highlight the matching button and show the
// corresponding navigator content (folders/contacts/calendar).
if (m_navMailBtn && m_navContactsBtn && m_navCalendarBtn && m_navStack) {
m_navMailBtn->blockSignals(true);
m_navContactsBtn->blockSignals(true);
m_navCalendarBtn->blockSignals(true);
switch (static_cast<Page>(pageIndex)) {
case PageMail:
m_navMailBtn->setChecked(true);
m_navStack->setCurrentIndex(0); // folder/category navigator
break;
case PageContacts:
m_navContactsBtn->setChecked(true);
m_navStack->setCurrentIndex(1); // contacts navigator
refreshContactsNavigator();
break;
case PageCalendar:
m_navCalendarBtn->setChecked(true);
m_navStack->setCurrentIndex(2); // mini calendar navigator
break;
default:
// Mail/Compose/Settings: keep the folder navigator highlighted.
m_navMailBtn->setChecked(true);
m_navStack->setCurrentIndex(0);
break;
}
m_navMailBtn->blockSignals(false);
m_navContactsBtn->blockSignals(false);
m_navCalendarBtn->blockSignals(false);
}
}
void MainMainWindow::refreshContactsNavigator()
{
if (!m_contactsNavList) return;
m_contactsNavList->clear();
// Aggregate contacts (sender + recipients) from local mails, most frequent first.
QHash<QString, int> contactCount; // email (lower) -> count
QHash<QString, QString> contactName; // email (lower) -> display label
QRegularExpression emailRx("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}");
auto ingest = [&](const QString &raw) {
QRegularExpressionMatch m = emailRx.match(raw);
if (!m.hasMatch()) return;
QString email = m.captured(0).toLower();
contactCount[email]++;
// Prefer "Name <email>" display; fallback to email.
if (!contactName.contains(email)) {
int lt = raw.indexOf('<');
QString cand = lt > 0 ? raw.left(lt).trimmed() : email;
contactName[email] = cand.isEmpty() ? email : cand;
}
};
for (const MailItem &mail : MailItemDao::findAll()) {
ingest(mail.sender());
ingest(mail.recipient());
ingest(mail.to());
ingest(mail.cc());
}
// Sort by frequency descending.
QStringList keys = contactCount.keys();
std::sort(keys.begin(), keys.end(), [&](const QString &a, const QString &b) {
return contactCount.value(a) > contactCount.value(b);
});
for (const QString &email : keys) {
QString label = contactName.value(email, email);
QListWidgetItem *item = new QListWidgetItem(label + " (" + QString::number(contactCount.value(email)) + ")",
m_contactsNavList);
item->setToolTip(email);
item->setData(Qt::UserRole, email);
m_contactsNavList->addItem(item);
}
} }
void MainMainWindow::onFolderSelected(const QModelIndex &index) void MainMainWindow::onFolderSelected(const QModelIndex &index)
+13 -2
View File
@@ -11,6 +11,8 @@
#include <QAction> #include <QAction>
#include <QToolButton> #include <QToolButton>
#include <QLabel> #include <QLabel>
#include <QListWidget>
#include <QCalendarWidget>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QWindowStateChangeEvent> #include <QWindowStateChangeEvent>
@@ -101,7 +103,7 @@ private slots:
void onEmbeddedDetachRequested(QWidget *widget); void onEmbeddedDetachRequested(QWidget *widget);
private: private:
void setupUI(); void setupUI();
void setupSidebar(); void setupNavigator(); // builds left navigator panel (folders/contacts/calendar + nav buttons)
void setupMailPage(); void setupMailPage();
void connectModels(); void connectModels();
void createTitleBar(); void createTitleBar();
@@ -110,9 +112,18 @@ private:
QWidget *createWindowControlButton(const QString &iconName, const QString &tooltip, std::function<void()> slot); QWidget *createWindowControlButton(const QString &iconName, const QString &tooltip, std::function<void()> slot);
void updateCategoryTreeForAccount(qint64 accountId); void updateCategoryTreeForAccount(qint64 accountId);
void assignCategoryToSelectedMails(qint64 categoryId); void assignCategoryToSelectedMails(qint64 categoryId);
void refreshContactsNavigator(); // populate the left-panel contact list
// Navigation // Navigation
QListWidget *m_sidebar; QWidget *m_navigatorPanel; // always-visible left panel
QStackedWidget *m_navStack; // navigator content: 0=folder/category, 1=contact list, 2=mini calendar
QToolButton *m_navMailBtn, *m_navContactsBtn, *m_navCalendarBtn;
// Navigators (content of the left panel, swapped per section)
QWidget *m_folderNav; // folders + categories (shown for Emails)
QWidget *m_contactsNav; // compact contact list
QWidget *m_calendarNav; // mini calendar widget
QListWidget *m_contactsNavList;
QCalendarWidget *m_calendarNavWidget;
QStackedWidget *m_stack; QStackedWidget *m_stack;
enum Page { enum Page {