feat: IMAP sync incremental + Account Setup Wizard + robust FETCH parser

- ImapSynchronizer::syncFolder(): detecta eliminados (UIDs locales no en servidor), fetch nuevos (sinceUid), actualiza flags (FLAGS batch)
- fetchAllUids(): SELECT + SEARCH ALL para lista completa UIDs servidor
- parseAndUpdateFlags(): FETCH FLAGS en lotes 100, update DB si cambió read/flagged
- AccountSetupDialog: integra ConnectionWizard para IMAP/POP3 con test de conexión real
- IMAP FETCH parser robusto: logging respuesta servidor + fallback UID-by-UID
- Fix: FETCH failed logging muestra first/last UID + respuesta truncada
This commit is contained in:
2026-08-17 15:34:49 +02:00
parent 364624aada
commit a004bdd60f
94 changed files with 19408 additions and 1047 deletions
+92 -22
View File
@@ -7,6 +7,7 @@
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QTimer>
#include "ui/connectionwizard.h"
// ─────────────────────── Constructor ───────────────────────
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
@@ -427,7 +428,8 @@ void AccountSetupDialog::onNextClicked()
if (page == PageProvider) {
if (m_selectedProvider == 2) {
goToPage(PageImap);
// Launch ConnectionWizard for IMAP/POP3 setup
launchConnectionWizard();
} else {
// Update OAuth page subtitle based on provider
goToPage(PageOAuth);
@@ -441,6 +443,20 @@ void AccountSetupDialog::onNextClicked()
}
}
void AccountSetupDialog::launchConnectionWizard()
{
ConnectionWizard wizard(this, m_accountService);
wizard.setWindowTitle("Configurar cuenta IMAP / POP3");
wizard.setWizardStyle(QWizard::ModernStyle);
connect(&wizard, &ConnectionWizard::settingsReady,
this, &AccountSetupDialog::onConnectionWizardFinished);
if (wizard.exec() == QDialog::Accepted) {
// Settings were emitted and handled in onConnectionWizardFinished
}
}
void AccountSetupDialog::onBackClicked()
{
int page = m_stack->currentIndex();
@@ -552,27 +568,81 @@ void AccountSetupDialog::submitImapAccount()
m_sslCheckbox->isChecked() ? "" : "No")
);
// Disable UI during connection attempt
m_btnBack->setEnabled(false);
m_btnNext->setEnabled(false);
m_btnCancel->setEnabled(false);
QString errorMsg;
bool success = m_accountService->testConnection(account.connectionSettings(), errorMsg);
if (success) {
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
m_btnCancel->setEnabled(true);
} else {
QMessageBox::warning(this, tr("Connection failed"), tr("Could not connect to IMAP server: %1").arg(errorMsg));
// Re-enable UI
m_btnBack->setEnabled(true);
m_btnNext->setEnabled(true);
m_btnCancel->setEnabled(true);
}
// Disable UI during connection attempt
m_btnBack->setEnabled(false);
m_btnNext->setEnabled(false);
m_btnCancel->setEnabled(false);
QString errorMsg;
bool success = m_accountService->testConnection(account.connectionSettings(), errorMsg);
if (success) {
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
m_btnCancel->setEnabled(true);
} else {
QMessageBox::warning(this, tr("Connection failed"), tr("Could not connect to IMAP server: %1").arg(errorMsg));
// Re-enable UI
m_btnBack->setEnabled(true);
m_btnNext->setEnabled(true);
m_btnCancel->setEnabled(true);
}
}
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onConnectionWizardFinished(const Account::ConnectionSettings &settings)
{
QString email = settings.username;
QString name = email.split("@").first();
// Show progress
m_accountCreatedOk = false;
goToPage(PageProgress);
m_progressIcon->setText("");
m_progressText->setText("Conectando al servidor...");
m_progressDetail->setText(
QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(settings.outgoingHost)
.arg(settings.outgoingPort)
.arg(settings.incomingSsl ? "" : "No")
);
// Disable UI during connection attempt
m_btnBack->setEnabled(false);
m_btnNext->setEnabled(false);
m_btnCancel->setEnabled(false);
QString errorMsg;
bool success = m_accountService->testConnection(settings, errorMsg);
if (success) {
// Create account with the connection settings
Account account;
account.setEmail(email);
account.setDisplayName(name);
// Determine account type from protocol
if (settings.type == "pop3") {
account.setType(AccountType::POP3);
} else {
account.setType(AccountType::IMAP);
}
account.setConnectionSettings(settings);
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
m_btnCancel->setEnabled(true);
} else {
showError(tr("Could not connect to server: %1").arg(errorMsg));
}
}
// ─────────────────── Account Result ───────────────────
+620
View File
@@ -0,0 +1,620 @@
#include "ui/accountsetupdialog.h"
#include <QMessageBox>
#include <QScrollArea>
#include <QFrame>
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QTimer>
// ─────────────────────── Constructor ───────────────────────
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
: QDialog(parent)
, m_accountService(accountService)
, m_selectedProvider(0)
, m_accountCreatedOk(false)
, m_editingAccountId(-1)
, m_isEditing(false)
{
m_authTimeoutTimer = new QTimer(this);
m_authTimeoutTimer->setSingleShot(true);
connect(m_authTimeoutTimer, &QTimer::timeout, this, &AccountSetupDialog::onAuthTimeout);
if (m_accountService) {
connect(m_accountService, &AccountService::accountAdded,
this, &AccountSetupDialog::onAccountAdded);
}
setupUI();
}
// ─────────────────────── Public API ───────────────────────
void AccountSetupDialog::loadAccountForEditing(const Account &account)
{
// Only support editing IMAP accounts for simplicity
if (account.type() != AccountType::IMAP) {
QMessageBox::information(this, tr("Edit Account"),
tr("Editing of OAuth (Gmail/Outlook) accounts is not supported in this version.\n"
"You can add a new account instead."));
m_isEditing = false;
return;
}
m_isEditing = true;
m_editingAccountId = account.id();
// Set window title
setWindowTitle(tr("Edit Email Account Wino Mail"));
// Fill IMAP fields
m_imapEmailEdit->setText(account.email());
m_imapNameEdit->setText(account.displayName());
// Password field left blank for security; user must re-enter
m_imapPasswordEdit->clear();
const Account::ConnectionSettings &settings = account.connectionSettings();
m_imapHostEdit->setText(settings.incomingHost);
m_imapPortEdit->setText(QString::number(settings.incomingPort));
m_smtpHostEdit->setText(settings.outgoingHost);
m_smtpPortEdit->setText(QString::number(settings.outgoingPort));
m_sslCheckbox->setChecked(settings.incomingSsl); // assuming same for SMTP
// Switch to IMAP page
m_selectedProvider = 2; // IMAP
goToPage(PageImap);
}
// ─────────────────────── UI Setup ───────────────────────
void AccountSetupDialog::setupUI()
{
setWindowTitle("Añadir Cuenta de Correo — Wino Mail");
setMinimumSize(600, 500);
resize(660, 520);
// ── Global stylesheet ──
setStyleSheet(
"AccountSetupDialog { background: #f5f5f7; }"
"QLabel { color: #1d1d1f; }"
"QLineEdit {"
" background: #fff; border: 1px solid #d1d1d6; border-radius: 6px;"
" padding: 8px 12px; font-size: 13px; color: #1d1d1f;"
"}"
"QLineEdit:focus { border: 2px solid #0071e3; }"
"QRadioButton { font-size: 14px; padding: 6px 0; }"
"QCheckBox { font-size: 13px; }"
"QPushButton {"
" font-weight: 600; font-size: 13px; padding: 8px 20px; border-radius: 6px;"
"}"
});
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(24, 24, 24, 18);
root->setSpacing(16);
// ── Stacked pages ──
m_stack = new QStackedWidget(this);
m_stack->addWidget(createProviderPage()); // 0
m_stack->addWidget(createOAuthPage()); // 1
m_stack->addWidget(createImapPage()); // 2
m_stack->addWidget(createProgressPage()); // 3
root->addWidget(m_stack, 1);
// ── Separator ──
QFrame *sep = new QFrame();
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet("color: #d1d1d6;");
root->addWidget(sep);
// ── Navigation buttons ──
QHBoxLayout *nav = new QHBoxLayout();
m_btnBack = new QPushButton("← Atrás");
m_btnBack->setStyleSheet(
"QPushButton { background: #fff; color: #0071e3; border: 1px solid #0071e3; }"
"QPushButton:hover { background: #e8f0fe; }"
);
m_btnCancel = new QPushButton("Cancelar");
m_btnCancel->setStyleSheet(
"QPushButton { background: #e5e5ea; color: #1d1d1f; border: none; }"
"QPushButton:hover { background: #d1d1d6; }"
);
m_btnNext = new QPushButton("Siguiente →");
m_btnNext->setStyleSheet(
"QPushButton { background: #0071e3; color: white; border: none; }"
"QPushButton:hover { background: #005bb5; }"
"QPushButton:disabled { background: #a0c4ff; color: #e0e0e0; }"
);
nav->addWidget(m_btnBack);
nav->addStretch();
nav->addWidget(m_btnCancel);
nav->addSpacing(8);
nav->addWidget(m_btnNext);
root->addLayout(nav);
connect(m_btnBack, &QPushButton::clicked, this, &AccountSetupDialog::onBackClicked);
connect(m_btnNext, &QPushButton::clicked, this, &AccountSetupDialog::onNextClicked);
connect(m_btnCancel, &QPushButton::clicked, this, &AccountSetupDialog::onCancelClicked);
goToPage(PageProvider);
}
// ─────────────────── Page 0: Provider ───────────────────
QWidget* AccountSetupDialog::createProviderPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(10, 10, 10, 10);
lay->setSpacing(20);
// Title
QLabel *title = new QLabel("Añadir nueva cuenta");
title->setStyleSheet("font-size: 22px; font-weight: 700; color: #1d1d1f;");
title->setAlignment(Qt::AlignCenter);
QLabel *subtitle = new QLabel("Selecciona el tipo de cuenta que deseas configurar:");
subtitle->setStyleSheet("font-size: 14px; color: #8e8e93;");
subtitle->setWordWrap(true);
subtitle->setAlignment(Qt::AlignCenter);
lay->addWidget(title);
lay->addWidget(subtitle);
lay->addSpacing(10);
// Provider cards
m_providerGroup = new QButtonGroup(this);
auto makeRadio = [&](const QString &text, const QString &desc, int id) -> QWidget* {
QWidget *card = new QWidget();
card->setStyleSheet(
"QWidget { background: #fff; border: 1px solid #e0e0e5; border-radius: 10px; }"
"QWidget:hover { border-color: #0071e3; }"
);
QHBoxLayout *h = new QHBoxLayout(card);
h->setContentsMargins(16, 12, 16, 12);
QRadioButton *radio = new QRadioButton(text);
radio->setStyleSheet("font-size: 15px; font-weight: 600;");
m_providerGroup->addButton(radio, id);
QLabel *descLabel = new QLabel(desc);
descLabel->setStyleSheet("font-size: 12px; color: #8e8e93;");
descLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
h->addWidget(radio, 1);
h->addWidget(descLabel);
return card;
};
lay->addWidget(makeRadio("🔴 Google / Gmail", "OAuth2 seguro", 0));
lay->addWidget(makeRadio("🔵 Microsoft / Outlook", "OAuth2 seguro", 1));
lay->addWidget(makeRadio("⚙️ IMAP / SMTP", "Servidor personalizado", 2));
m_providerGroup->button(0)->setChecked(true);
connect(m_providerGroup, QOverload<int>::of(&QButtonGroup::idClicked),
this, &AccountSetupDialog::onProviderSelected);
lay->addStretch();
return page;
}
// ─────────────────── Page 1: OAuth ───────────────────
QWidget* AccountSetupDialog::createOAuthPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(10, 10, 10, 10);
lay->setSpacing(14);
QLabel *title = new QLabel("Autenticación OAuth2");
title->setStyleSheet("font-size: 18px; font-weight: 700;");
lay->addWidget(title);
QLabel *info = new QLabel(
"Se abrirá tu navegador web para que inicies sesión de forma segura.\\n"
"Wino Mail no almacena tu contraseña, solo el token de acceso autorizado.\\n\\n"
"Pasos:\\n"
" 1. Introduce tu dirección de correo.\\n"
" 2. Pulsa «Autenticar en Navegador».\\n"
" 3. Completa el inicio de sesión en la ventana del navegador.\\n"
" 4. Al terminar, esta ventana se actualizará automáticamente."
);
info->setWordWrap(true);
info->setStyleSheet("font-size: 13px; color: #555; line-height: 1.5;");
lay->addWidget(info);
lay->addSpacing(6);
QLabel *emailLabel = new QLabel("Correo electrónico:");
emailLabel->setStyleSheet("font-weight: 600; font-size: 13px;");
lay->addWidget(emailLabel);
m_oauthEmailEdit = new QLineEdit();
m_oauthEmailEdit->setPlaceholderText("tu-correo@gmail.com");
QRegularExpression rx(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
m_oauthEmailEdit->setValidator(new QRegularExpressionValidator(rx, this));
lay->addWidget(m_oauthEmailEdit);
lay->addSpacing(8);
m_btnOAuthStart = new QPushButton("🔑 Autenticar en Navegador");
m_btnOAuthStart->setMinimumHeight(40);
m_btnOAuthStart->setStyleSheet(
"QPushButton { background: #34a853; color: white; border: none; font-size: 14px; font-weight: 700; border-radius: 8px; }"
"QPushButton:hover { background: #2d9249; }"
"QPushButton:disabled { background: #a8d5b8; color: #e0e0e0; }"
);
connect(m_btnOAuthStart, &QPushButton::clicked, this, &AccountSetupDialog::startOAuthAuthentication);
lay->addWidget(m_btnOAuthStart);
m_oauthStatusLabel = new QLabel("Esperando…");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #8e8e93; font-style: italic;");
m_oauthStatusLabel->setAlignment(Qt::AlignCenter);
lay->addWidget(m_oauthStatusLabel);
lay->addStretch();
return page;
}
// ─────────────────── Page 2: IMAP ───────────────────
QWidget* AccountSetupDialog::createImapPage()
{
QWidget *page = new QWidget();
QVBoxLayout *outerLay = new QVBoxLayout(page);
outerLay->setContentsMargins(10, 10, 10, 10);
QLabel *title = new QLabel("Configuración IMAP / SMTP");
title->setStyleSheet("font-size: 18px; font-weight: 700;");
outerLay->addWidget(title);
QLabel *info = new QLabel("Introduce los datos de tu servidor de correo. "
"Consulta a tu proveedor si no conoces los datos del servidor.");
info->setWordWrap(true);
info->setStyleSheet("font-size: 12px; color: #8e8e93; margin-bottom: 8px;");
outerLay->addWidget(info);
// Scrollable form
QScrollArea *scroll = new QScrollArea();
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
QWidget *formWidget = new QWidget();
QFormLayout *form = new QFormLayout(formWidget);
form->setSpacing(10);
form->setContentsMargins(0, 8, 12, 8);
form->setLabelAlignment(Qt::AlignRight);
m_imapEmailEdit = new QLineEdit();
m_imapEmailEdit->setPlaceholderText("usuario@empresa.com");
m_imapNameEdit = new QLineEdit();
m_imapNameEdit->setPlaceholderText("Nombre a mostrar (ej. Javier)");
m_imapPasswordEdit = new QLineEdit();
m_imapPasswordEdit->setEchoMode(QLineEdit::Password);
m_imapPasswordEdit->setPlaceholderText("Contraseña");
m_imapHostEdit = new QLineEdit();
m_imapHostEdit->setPlaceholderText("imap.empresa.com");
m_imapPortEdit = new QLineEdit();
m_imapPortEdit->setPlaceholderText("993");
m_imapPortEdit->setValidator(new QIntValidator(1, 65535, this));
m_imapPortEdit->setMaximumWidth(100);
m_smtpHostEdit = new QLineEdit();
m_smtpHostEdit->setPlaceholderText("smtp.empresa.com");
m_smtpPortEdit = new QLineEdit();
m_smtpPortEdit->setPlaceholderText("587");
m_smtpPortEdit->setValidator(new QIntValidator(1, 65535, this));
m_smtpPortEdit->setMaximumWidth(100);
m_sslCheckbox = new QCheckBox("Usar conexión segura (SSL/TLS)");
m_sslCheckbox->setChecked(true);
form->addRow("Correo:", m_imapEmailEdit);
form->addRow("Nombre:", m_imapNameEdit);
form->addRow("Contraseña:", m_imapPasswordEdit);
// Visual separator
QFrame *line = new QFrame();
line->setFrameShape(QFrame::HLine);
line->setStyleSheet("color: #e0e0e5;");
form->addRow(line);
QLabel *serverHeader = new QLabel("Servidores");
serverHeader->setStyleSheet("font-weight: 700; font-size: 13px; color: #0071e3;");
form->addRow(serverHeader);
form->addRow("Servidor IMAP:", m_imapHostEdit);
form->addRow("Puerto IMAP:", m_imapPortEdit);
form->addRow("Servidor SMTP:", m_smtpHostEdit);
form->addRow("Puerto SMTP:", m_smtpPortEdit);
form->addRow("", m_sslCheckbox);
scroll->setWidget(formWidget);
outerLay->addWidget(scroll, 1);
return page;
}
// ─────────────────── Page 3: Progress ───────────────────
QWidget* AccountSetupDialog::createProgressPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(20, 40, 20, 20);
lay->setAlignment(Qt::AlignCenter);
m_progressIcon = new QLabel("⏳");
m_progressIcon->setStyleSheet("font-size: 56px;");
m_progressIcon->setAlignment(Qt::AlignCenter);
m_progressText = new QLabel("Conectando al servidor…");
m_progressText->setStyleSheet("font-size: 16px; font-weight: 600; color: #1d1d1f;");
m_progressText->setAlignment(Qt::AlignCenter);
m_progressDetail = new QLabel("Validando credenciales y configuración del servidor de correo.");
m_progressDetail->setWordWrap(true);
m_progressDetail->setStyleSheet("font-size: 13px; color: #8e8e93;");
m_progressDetail->setAlignment(Qt::AlignCenter);
lay->addWidget(m_progressIcon);
lay->addSpacing(16);
lay->addWidget(m_progressText);
lay->addSpacing(8);
lay->addWidget(m_progressDetail);
lay->addStretch();
return page;
}
// ─────────────────── Navigation ───────────────────
void AccountSetupDialog::goToPage(int page)
{
m_stack->setCurrentIndex(page);
updateNavButtons();
}
void AccountSetupDialog::updateNavButtons()
{
int page = m_stack->currentIndex();
m_btnBack->setVisible(page > 0 && page != PageProgress);
m_btnCancel->setVisible(page != PageProgress || !m_accountCreatedOk);
switch (page) {
case PageProvider:
m_btnNext->setText("Siguiente →");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
break;
case PageOAuth:
m_btnNext->setVisible(false); // OAuth flow is driven by the authenticate button
break;
case PageImap:
m_btnNext->setText(m_isEditing ? "Guardar cambios" : "Conectar");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
break;
case PageProgress:
if (m_accountCreatedOk) {
m_btnNext->setText("Finalizar ✓");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
m_btnBack->setVisible(false);
m_btnCancel->setVisible(false);
} else {
m_btnNext->setVisible(false);
}
break;
}
}
void AccountSetupDialog::onProviderSelected(int id)
{
m_selectedProvider = id;
}
void AccountSetupDialog::onNextClicked()
{
int page = m_stack->currentIndex();
if (page == PageProvider) {
if (m_selectedProvider == 2) {
goToPage(PageImap);
} else {
// Update OAuth page subtitle based on provider
goToPage(PageOAuth);
}
}
else if (page == PageImap) {
submitImapAccount();
}
else if (page == PageProgress) {
accept(); // Finalizar
}
}
void AccountSetupDialog::onBackClicked()
{
int page = m_stack->currentIndex();
if (page == PageOAuth || page == PageImap) {
m_authTimeoutTimer->stop();
goToPage(PageProvider);
}
}
void AccountSetupDialog::onCancelClicked()
{
m_authTimeoutTimer->stop();
reject();
}
// ─────────────────── OAuth Flow ───────────────────
void AccountSetupDialog::startOAuthAuthentication()
{
QString email = m_oauthEmailEdit->text().trimmed();
if (email.isEmpty()) {
QMessageBox::warning(this, "Validación",
"Introduce tu dirección de correo electrónico.");
return;
}
m_btnOAuthStart->setEnabled(false);
m_oauthStatusLabel->setText("Abriendo navegador… Completa el inicio de sesión allí.");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #0071e3; font-weight: 600;");
QString provider = (m_selectedProvider == 0) ? "gmail" : "outlook";
// Start authentication via AccountService (opens browser)
m_accountService->startAuthentication(email, provider);
// Start timeout timer (120 seconds to complete OAuth)
m_authTimeoutTimer->start(120000);
qDebug() << "[AccountSetupDialog] OAuth started for" << email << "provider:" << provider;
}
void AccountSetupDialog::onAuthTimeout()
{
if (m_stack->currentIndex() == PageOAuth) {
m_btnOAuthStart->setEnabled(true);
m_oauthStatusLabel->setText("⚠️ Tiempo de espera agotado. Inténtalo de nuevo.");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #ff3b30; font-weight: 600;");
}
}
// ─────────────────── IMAP Flow ───────────────────
void AccountSetupDialog::submitImapAccount()
{
QString email = m_imapEmailEdit->text().trimmed();
QString name = m_imapNameEdit->text().trimmed();
QString password = m_imapPasswordEdit->text().trimmed();
QString imapHost = m_imapHostEdit->text().trimmed();
QString imapPort = m_imapPortEdit->text().trimmed();
QString smtpHost = m_smtpHostEdit->text().trimmed();
QString smtpPort = m_smtpPortEdit->text().trimmed();
// Validation
if (email.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce tu dirección de correo.");
return;
}
if (password.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce tu contraseña.");
return;
}
if (imapHost.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce el servidor IMAP.");
return;
}
// Use defaults if ports are empty
if (imapPort.isEmpty()) imapPort = m_sslCheckbox->isChecked() ? "993" : "143";
if (smtpPort.isEmpty()) smtpPort = m_sslCheckbox->isChecked() ? "465" : "587";
if (smtpHost.isEmpty()) smtpHost = imapHost.replace("imap.", "smtp.");
if (name.isEmpty()) name = email.split("@").first();
// Create account with connection settings
Account account;
account.setEmail(email);
account.setDisplayName(name);
account.setType(AccountType::IMAP);
Account::ConnectionSettings settings;
settings.type = "imap";
settings.incomingHost = imapHost;
settings.incomingPort = imapPort.toInt();
settings.incomingSsl = m_sslCheckbox->isChecked();
settings.outgoingHost = smtpHost;
settings.outgoingPort = smtpPort.toInt();
settings.outgoingSsl = m_sslCheckbox->isChecked();
settings.username = email;
settings.password = password;
settings.authMethod = "plain";
account.setConnectionSettings(settings);
// Show progress
m_accountCreatedOk = false;
goToPage(PageProgress);
m_progressIcon->setText("⏳");
m_progressText->setText("Conectando al servidor...");
m_progressDetail->setText(
QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5")
.arg(imapHost, imapPort, smtpHost, smtpPort,
m_sslCheckbox->isChecked() ? "Sí" : "No")
);
// Test connection
QString errorMsg;
if (!m_accountService->testConnection(account.connectionSettings(), errorMsg)) {
showError(tr("Connection test failed: %1").arg(errorMsg));
m_authTimeoutTimer->stop();
return;
}
// Connection OK, proceed to add account
m_progressText->setText(tr("Guardando cuenta..."));
m_progressDetail->setText("");
// Start timeout for safety
m_authTimeoutTimer->start(30000);
// Perform add/update after a short delay to let UI update
QTimer::singleShot(0, this, [this, account]() mutable {
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
}
});
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onAccountAdded(const Account &account)
{
m_authTimeoutTimer->stop();
m_accountCreatedOk = true;
qDebug() << "[AccountSetupDialog] Account" << (m_isEditing ? "updated" : "created") << "successfully:" << account.email();
// If we were on the OAuth page, move to progress first
if (m_stack->currentIndex() == PageOAuth) {
goToPage(PageProgress);
}
showSuccess(QString(m_isEditing ? "¡Cuenta «%1» actualizada con éxito!\\n\\n"
"Los cambios se aplicarán inmediatamente."
: "¡Cuenta «%1» configurada con éxito!\\n\\n"
"La sincronización de correos comenzará automáticamente en segundo plano.")
.arg(account.email()));
emit accountCreated(account);
}
void AccountSetupDialog::showSuccess(const QString &message)
{
m_progressIcon->setText("✅");
m_progressText->setText("¡Cuenta añadida!");
m_progressDetail->setText(message);
updateNavButtons();
}
void AccountSetupDialog::showError(const QString &message)
{
m_accountCreatedOk = false;
m_progressIcon->setText("❌");
m_progressText->setText("Error de conexión");
m_progressDetail->setText(message);
// Allow going back
m_btnBack->setVisible(true);
m_btnCancel->setVisible(true);
m_btnNext->setVisible(false);
}
#include "accountsetupdialog.moc"
+2
View File
@@ -40,6 +40,7 @@ private slots:
void startOAuthAuthentication();
void onAccountAdded(const Account &account);
void onAuthTimeout();
void onConnectionWizardFinished(const Account::ConnectionSettings &settings);
private:
void setupUI();
@@ -53,6 +54,7 @@ private:
void showError(const QString &message);
void showSuccess(const QString &message);
void submitImapAccount();
void launchConnectionWizard();
// Services
AccountService *m_accountService;
+78
View File
@@ -0,0 +1,78 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# 1. Find the line where deleteAction is declared
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
# Insert flag action declaration right after it
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# 2. Find the closing brace of the createToolBar function
brace_line = None
brace_count = 0
in_function = False
for i, line in enumerate(lines):
if i < decl_line:
continue
if 'void MainMainWindow::createToolBar() {' in line:
in_function = True
continue
if not in_function:
continue
# Count braces
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0:
brace_line = i
break
if brace_line is not None:
break
if brace_line is None:
print('Could not find closing brace of createToolBar')
sys.exit(1)
# 3. Insert flag connection just before the closing brace
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh(); // optional, to update icon if any
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:brace_line] + [flag_conn] + lines[brace_line:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Flag action added')
+240 -413
View File
@@ -1,277 +1,21 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
// ===================== ComposeView =====================
#include "composeview.h"
#include <QDialog>
#include <QInputDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QLabel>
#include "tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPixmap>
#include <QStyle>
#include <QApplication>
#include <QFileIconProvider>
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
@@ -282,14 +26,14 @@ ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested()
{
void ComposeView::onSignatureEditRequested() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Edit Signature"));
dialog.setMinimumSize(600, 400);
@@ -314,14 +58,13 @@ void ComposeView::onSignatureEditRequested()
}
}
}
void ComposeView::setAccountService(AccountService *service)
{
void ComposeView::setAccountService(AccountService *service) {
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
void ComposeView::populateAccountCombo() {
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
@@ -342,8 +85,7 @@ void ComposeView::populateAccountCombo()
}
}
void ComposeView::onAccountChanged(int index)
{
void ComposeView::onAccountChanged(int index) {
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
@@ -354,8 +96,7 @@ void ComposeView::onAccountChanged(int index)
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
void ComposeView::loadSignatureForCurrentAccount() {
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
@@ -367,12 +108,12 @@ void ComposeView::loadSignatureForCurrentAccount()
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
delete account;
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
@@ -383,37 +124,9 @@ void ComposeView::setupUI() {
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
// De: field + Account selector (renamed from From:)
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
QLabel *fromLabel = new QLabel(tr("De:"));
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
@@ -429,16 +142,17 @@ void ComposeView::setupUI() {
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
fromLayout->addWidget(m_accountCombo);
fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
// Para: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
QLabel *toLabel = new QLabel(tr("Para:"));
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new everload_tags::TagsLineEdit();
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
@@ -466,19 +180,20 @@ void ComposeView::setupUI() {
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
QLabel *ccLabel = new QLabel(tr("Cc:"));
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
m_ccField = new everload_tags::TagsLineEdit();
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton = new QPushButton("");
m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setToolTip(tr("Hide Cc"));
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
@@ -491,19 +206,20 @@ void ComposeView::setupUI() {
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
QLabel *bccLabel = new QLabel(tr("Bcc:"));
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
m_bccField = new everload_tags::TagsLineEdit();
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton = new QPushButton("");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideBccButton->setToolTip(tr("Hide Bcc"));
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
@@ -512,11 +228,39 @@ void ComposeView::setupUI() {
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Asunto: field
QHBoxLayout *subjectLayout = new QHBoxLayout();
QLabel *subjectLabel = new QLabel(tr("Asunto:"));
subjectLabel->setFixedWidth(60);
subjectLabel->setStyleSheet("font-weight: bold; color: #555;");
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
//subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
// Detach button placed to the right of the subject line
m_detachButton = new QPushButton("");//("↥ Detach");
m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg")));
m_detachButton->setToolTip(tr("Open compose window in a separate window"));
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
subjectLayout->addWidget(m_detachButton);
mainLayout->addLayout(subjectLayout);
// Separator line admin@email.com
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
@@ -527,7 +271,7 @@ void ComposeView::setupUI() {
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
QLabel *scheduleLabel = new QLabel(tr("Send at:"));
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
@@ -536,79 +280,91 @@ void ComposeView::setupUI() {
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton = new QPushButton(tr("Schedule Send"));
m_scheduleSendButton->setFixedWidth(150);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
scheduleLayout->addWidget(m_scheduleSendButton, 2);
m_schedulePanel->setVisible(false);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Attachment section
QHBoxLayout *attachmentLayout = new QHBoxLayout();
QLabel *attachmentLabel = new QLabel(tr("Attachments:"));
attachmentLabel->setFixedWidth(80);
attachmentLabel->setStyleSheet("font-weight: bold; color: #555;");
attachmentLayout->addWidget(attachmentLabel);
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
qDebug() << "Attach button connected:" << connected;
attachmentLayout->addWidget(m_attachButton);
m_attachmentList = new QListWidget();
m_attachmentList->setViewMode(QListView::IconMode);
m_attachmentList->setResizeMode(QListView::Adjust);
m_attachmentList->setMovement(QListView::Static);
m_attachmentList->setGridSize(QSize(300, 50));
m_attachmentList->setSpacing(10);
m_attachmentList->setLayoutDirection(Qt::LeftToRight);
m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection);
m_attachmentList->setMaximumHeight(60);
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }");
m_attachmentList->setStyleSheet(
"QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }"
"QListWidget::item { border: none; padding: 5px; }"
"QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1976D2, stop:1 #1565C0); border-radius: 4px; }"
);
m_attachmentList->setVisible(false);
attachmentLayout->addWidget(m_attachmentList, 1);
m_removeAttachmentButton = new QToolButton();
m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg")));
m_removeAttachmentButton->setToolTip(tr("Remove selected attachment"));
m_removeAttachmentButton->setIconSize(QSize(20,20));
m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked);
attachmentLayout->addWidget(m_removeAttachmentButton);
mainLayout->addLayout(attachmentLayout);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
// Attachments and Templates buttons (right-aligned)
QHBoxLayout *buttonRow = new QHBoxLayout();
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
buttonRow->addWidget(m_attachButton);
m_templateButton = new QToolButton();
m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Files/File Text.svg"))); // Assuming you have a template icon
m_templateButton->setToolTip(tr("Email templates"));
m_templateButton->setIconSize(QSize(20,20));
m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_templateButton, &QToolButton::clicked, this, &ComposeView::onTemplateClicked);
buttonRow->addWidget(m_templateButton);
buttonRow->setSpacing(4);
actionsLayout->addLayout(buttonRow);
actionsLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
m_discardButton = new QPushButton(tr("Discard"));
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
actionsLayout->addWidget(m_discardButton);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg")));
m_templateButton->setIconSize(QSize(20,20));
//m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 20px;}"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
@@ -633,25 +389,27 @@ void ComposeView::onBccToggle() {
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_toField->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_toField->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
@@ -659,15 +417,19 @@ void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setTo(const QString &to) { m_toField->tags(to.split(',', Qt::SkipEmptyParts)); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_toField->tags(QStringList{});
m_ccField->tags(QStringList{});
m_bccField->tags(QStringList{});
m_subjectField->clear();
m_bodyEditor->clear();
m_attachmentFiles.clear();
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
@@ -675,35 +437,100 @@ void ComposeView::initializeComposition() {
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->tags(initialRecipient.split(',', Qt::SkipEmptyParts));
m_toField->setFocus();
}
void ComposeView::onAddAttachmentClicked()
{
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList);
item->setToolTip(file);
}
QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count()));
}
void ComposeView::onAddAttachmentClicked() {
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(m_attachmentList);
item->setData(Qt::UserRole, file);
QWidget *widget = new QWidget;
widget->setFixedSize(300, 50);
widget->setStyleSheet(
"background-color: #f8f9fa; "
"border-radius: 8px; "
"border: 1px solid #e9ecef;"
);
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(8, 4, 8, 4);
layout->setSpacing(8);
// Icon from system
QFileIconProvider provider;
QIcon icon = provider.icon(QFileInfo(file));
QLabel *iconLabel = new QLabel;
QPixmap pix = icon.pixmap(32, 32);
if (pix.isNull())
pix = QIcon::fromTheme("unknown").pixmap(32,32);
iconLabel->setPixmap(pix);
iconLabel->setFixedSize(32,32);
layout->addWidget(iconLabel);
// Text vertical layout
QVBoxLayout *textLayout = new QVBoxLayout;
textLayout->setSpacing(2);
QString fileName = QFileInfo(file).fileName();
QLabel *nameLabel = new QLabel(fileName);
nameLabel->setStyleSheet("font-weight: bold;");
QLabel *sizeLabel = new QLabel(tr("%1 KB").arg(QFileInfo(file).size()/1024));
sizeLabel->setStyleSheet("color: #666; font-size: 10px;");
textLayout->addWidget(nameLabel);
textLayout->addWidget(sizeLabel);
layout->addLayout(textLayout);
layout->addStretch();
// Delete button
QPushButton *delBtn = new QPushButton;
QIcon delIcon = QIcon::fromTheme("edit-delete");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("list-remove");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("gtk-delete");
delBtn->setIcon(delIcon);
delBtn->setToolTip(tr("Remove attachment"));
delBtn->setFixedSize(24,24);
delBtn->setStyleSheet(
"QPushButton { border: none; background: transparent; }"
"QPushButton:hover { background: #e9ecef; border-radius: 4px; }"
);
layout->addWidget(delBtn);
// Connect delete button
connect(delBtn, &QPushButton::clicked, [this, item]() {
int row = m_attachmentList->row(item);
if (row >= 0) {
QListWidgetItem *it = m_attachmentList->takeItem(row);
if (it) {
m_attachmentFiles.removeAt(row);
delete it;
}
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
});
m_attachmentList->addItem(item);
m_attachmentList->setItemWidget(item, widget);
}
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onRemoveAttachmentClicked()
{
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
int row = m_attachmentList->row(item);
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
}
void ComposeView::onRemoveAttachmentClicked() {
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
int row = m_attachmentList->row(item);
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
#include "composeview.moc"
void ComposeView::onTemplateClicked() {
// Placeholder for template functionality
QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented."));
}
#include "composeview.moc"
+632
View File
@@ -0,0 +1,632 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
#include "composeview.moc"
+691
View File
@@ -0,0 +1,691 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr("From:"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr("Detach compose window"));
m_detachButton->setText(QStringLiteral("⤢"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 6px; color: #555; font-size: 14px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr("To:"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr("To recipients"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr("CC"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr("BCC"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr("Cc:"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr("Cc recipients"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr("Bcc:"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr("Bcc recipients"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr("Subject:"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr("Send at:"), this);
scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr("Discard"), this);
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join("; "));
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
#include "composeview.moc"
+691
View File
@@ -0,0 +1,691 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr("From:"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr("Detach compose window"));
m_detachButton->setText(QStringLiteral("⤢"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 6px; color: #555; font-size: 14px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr("To:"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr("To recipients"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr("CC"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr("BCC"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr("Cc:"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr("Cc recipients"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr("Bcc:"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr("Bcc recipients"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr("Subject:"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr("Send at:"), this);
scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr("Discard"), this);
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join("; "));
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
#include "composeview.moc"
+698
View File
@@ -0,0 +1,698 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include "tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Attachment section
QHBoxLayout *attachmentLayout = new QHBoxLayout();
QLabel *attachmentLabel = new QLabel(tr("Attachments:"));
attachmentLabel->setFixedWidth(80);
attachmentLabel->setStyleSheet("font-weight: bold; color: #555;");
attachmentLayout->addWidget(attachmentLabel);
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
qDebug() << "Attach button connected:" << connected;
attachmentLayout->addWidget(m_attachButton);
m_attachmentList = new QListWidget();
m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection);
m_attachmentList->setMaximumHeight(60);
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }");
attachmentLayout->addWidget(m_attachmentList, 1);
m_removeAttachmentButton = new QToolButton();
m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg")));
m_removeAttachmentButton->setToolTip(tr("Remove selected attachment"));
m_removeAttachmentButton->setIconSize(QSize(20,20));
m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked);
attachmentLayout->addWidget(m_removeAttachmentButton);
mainLayout->addLayout(attachmentLayout);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
void ComposeView::onAddAttachmentClicked()
{
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList);
item->setToolTip(file);
}
QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count()));
}
void ComposeView::onRemoveAttachmentClicked()
{
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
int row = m_attachmentList->row(item);
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
}
#include "composeview.moc"
+632
View File
@@ -0,0 +1,632 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
#include "composeview.moc"
+12 -41
View File
@@ -17,45 +17,12 @@
#include <QComboBox>
#include <QListWidget>
#include "models/EmailCompositionModel.h"
#include "tags_line_edit.hpp"
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include "services/accountservice.h"
class AccountService;
class RichTextEditor : public QTextEdit {
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
};
#include "richtexteditor.h"
class ComposeView : public QWidget {
Q_OBJECT
@@ -71,7 +38,8 @@ signals:
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress);
const QString &fromAddress,
const QStringList &attachmentPaths);
void discardRequested();
public slots:
@@ -92,6 +60,7 @@ private slots:
void onSignatureEditRequested();
void onAddAttachmentClicked();
void onRemoveAttachmentClicked();
void onTemplateClicked(); // new slot for templates
private:
void setupUI();
@@ -101,15 +70,17 @@ private:
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLineEdit *m_toField = nullptr;
QLabel *m_fromLabel = nullptr;
QComboBox *m_accountCombo = nullptr;
everload_tags::TagsLineEdit *m_toField = nullptr;
everload_tags::TagsLineEdit *m_ccField = nullptr;
everload_tags::TagsLineEdit *m_bccField = nullptr;
QLineEdit *m_subjectField = nullptr;
RichTextEditor *m_bodyEditor = nullptr;
QPushButton *m_detachButton = nullptr;
QWidget *m_ccRow = nullptr;
QWidget *m_bccRow = nullptr;
QWidget *m_schedulePanel = nullptr;
QLineEdit *m_ccField = nullptr;
QLineEdit *m_bccField = nullptr;
QPushButton *m_ccButton = nullptr;
QPushButton *m_bccButton = nullptr;
QPushButton *m_hideCcButton = nullptr;
@@ -121,15 +92,15 @@ private:
QAction *m_sendNowAction = nullptr;
QAction *m_scheduleAction = nullptr;
QToolButton *m_sendSplit = nullptr;
QComboBox *m_accountCombo = nullptr;
bool m_ccVisible = false;
bool m_bccVisible = false;
int m_currentAccountId = -1;
// Attachment UI
QToolButton *m_attachButton = nullptr;
QToolButton *m_templateButton = nullptr;
QListWidget *m_attachmentList = nullptr;
QToolButton *m_removeAttachmentButton = nullptr;
QStringList m_attachmentFiles;
};
#endif // COMPOSE_VIEW_H
#endif // COMPOSE_VIEW_H
+127
View File
@@ -0,0 +1,127 @@
#ifndef COMPOSE_VIEW_H
#define COMPOSE_VIEW_H
#include <QWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QToolBar>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QFrame>
#include <QMenu>
#include <QAction>
#include <QToolButton>
#include <QDateTimeEdit>
#include <QFontComboBox>
#include <QSpinBox>
#include <QComboBox>
#include "models/EmailCompositionModel.h"
#include "services/accountservice.h"
class AccountService;
class RichTextEditor : public QTextEdit {
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
};
class ComposeView : public QWidget {
Q_OBJECT
public:
explicit ComposeView(QWidget *parent = nullptr);
~ComposeView() override;
void setAccountService(AccountService *service);
signals:
void compositionFinished();
void detachRequested(QWidget *widget);
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress);
void discardRequested();
public slots:
void initializeComposition();
void startNewEmail(const QString &initialRecipient);
void setTo(const QString &to);
void setSubject(const QString &subject);
void setBody(const QString &body);
private slots:
void onCcToggle();
void onBccToggle();
void onSendClicked();
void onScheduleClicked();
void onDetachClicked();
void onAccountChanged(int index);
void onSignatureClicked();
void onSignatureEditRequested();
private:
void setupUI();
void populateAccountCombo();
void loadSignatureForCurrentAccount();
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLineEdit *m_toField = nullptr;
QLineEdit *m_subjectField = nullptr;
RichTextEditor *m_bodyEditor = nullptr;
QPushButton *m_detachButton = nullptr;
QWidget *m_ccRow = nullptr;
QWidget *m_bccRow = nullptr;
QWidget *m_schedulePanel = nullptr;
QLineEdit *m_ccField = nullptr;
QLineEdit *m_bccField = nullptr;
QPushButton *m_ccButton = nullptr;
QPushButton *m_bccButton = nullptr;
QPushButton *m_hideCcButton = nullptr;
QPushButton *m_hideBccButton = nullptr;
QDateTimeEdit *m_schedulePicker = nullptr;
QPushButton *m_scheduleSendButton = nullptr;
QPushButton *m_discardButton = nullptr;
QMenu *m_sendMenu = nullptr;
QAction *m_sendNowAction = nullptr;
QAction *m_scheduleAction = nullptr;
QToolButton *m_sendSplit = nullptr;
QComboBox *m_accountCombo = nullptr;
bool m_ccVisible = false;
bool m_bccVisible = false;
int m_currentAccountId = -1;
};
#endif // COMPOSE_VIEW_H
+224
View File
@@ -0,0 +1,224 @@
#include "ui/connectionwizard.h"
#include <QMessageBox>
#include <QTimer>
#include <QDebug>
#include <QSslSocket>
ConnectionWizard::ConnectionWizard(QWidget *parent, AccountService *accountService)
: QWizard(parent), m_accountService(accountService)
{
setWindowTitle("Configurar conexión de correo");
setWizardStyle(QWizard::ModernStyle);
setOption(QWizard::HaveHelpButton, false);
setOption(QWizard::HaveFinishButtonOnEarlyPages, true);
setupPageIntro();
setupPageIncoming();
setupPageOutgoing();
setupPageAuth();
setupPageTest();
// Set page order
addPage(m_introPage);
addPage(m_incomingPage);
addPage(m_outgoingPage);
addPage(m_authPage);
addPage(m_testPage);
// Start at intro
startId();
}
void ConnectionWizard::setupPageIntro()
{
m_introPage = new QWizardPage(this);
m_introPage->setTitle("Configuración de conexión");
m_introPage->setSubTitle("Introduce los datos de los servidores entrantes y salientes para configurar tu cuenta de correo.");
QVBoxLayout *lay = new QVBoxLayout(m_introPage);
QLabel *info = new QLabel(
"Este asistente te guiará paso a paso para configurar los servidores IMAP/POP3 y SMTP.\\n"
"Si no conoces los datos, consulta a tu proveedor de correo o busca en sus páginas de soporte."
);
info->setWordWrap(true);
lay->addWidget(info);
lay->addStretch();
m_introPage->setLayout(lay);
}
void ConnectionWizard::setupPageIncoming()
{
m_incomingPage = new QWizardPage(this);
m_incomingPage->setTitle("Servidor entrante (IMAP/POP3)");
m_incomingPage->setSubTitle("Configura el servidor que recibirá tus correos.");
QFormLayout *form = new QFormLayout();
m_incomingTypeCombo = new QComboBox();
m_incomingTypeCombo->addItem("IMAP", QVariant::fromValue(QString("imap")));
m_incomingTypeCombo->addItem("POP3", QVariant::fromValue(QString("pop3")));
form->addRow("Tipo:", m_incomingTypeCombo);
m_incomingHostEdit = new QLineEdit();
m_incomingHostEdit->setPlaceholderText("ej. imap.ejemplo.com");
form->addRow("Servidor:", m_incomingHostEdit);
m_incomingPortEdit = new QLineEdit();
m_incomingPortEdit->setPlaceholderText("993");
m_incomingPortEdit->setValidator(new QIntValidator(1, 65535, this));
form->addRow("Puerto:", m_incomingPortEdit);
m_incomingSslCheck = new QCheckBox("Usar conexión segura (SSL/TLS)");
m_incomingSslCheck->setChecked(true);
form->addRow("", m_incomingSslCheck);
m_incomingPage->setLayout(form);
}
void ConnectionWizard::setupPageOutgoing()
{
m_outgoingPage = new QWizardPage(this);
m_outgoingPage->setTitle("Servidor saliente (SMTP)");
m_outgoingPage->setSubTitle("Configura el servidor que enviará tus correos.");
QFormLayout *form = new QFormLayout();
m_outgoingHostEdit = new QLineEdit();
m_outgoingHostEdit->setPlaceholderText("ej. smtp.ejemplo.com");
form->addRow("Servidor:", m_outgoingHostEdit);
m_outgoingPortEdit = new QLineEdit();
m_outgoingPortEdit->setPlaceholderText("587");
m_outgoingPortEdit->setValidator(new QIntValidator(1, 65535, this));
form->addRow("Puerto:", m_outgoingPortEdit);
m_outgoingSslCheck = new QCheckBox("Usar conexión segura (STARTTLS/SSL)");
m_outgoingSslCheck->setChecked(true);
form->addRow("", m_outgoingSslCheck);
m_outgoingPage->setLayout(form);
}
void ConnectionWizard::setupPageAuth()
{
m_authPage = new QWizardPage(this);
m_authPage->setTitle("Autenticación");
m_authPage->setSubTitle("Introduce tu nombre de usuario y contraseña para acceder a los servidores.");
QFormLayout *form = new QFormLayout();
m_usernameEdit = new QLineEdit();
m_usernameEdit->setPlaceholderText("tu-usuario@dominio.com");
form->addRow("Usuario:", m_usernameEdit);
m_passwordEdit = new QLineEdit();
m_passwordEdit->setEchoMode(QLineEdit::Password);
m_passwordEdit->setPlaceholderText("Contraseña");
form->addRow("Contraseña:", m_passwordEdit);
m_authMethodCombo = new QComboBox();
m_authMethodCombo->addItem("Contraseña normal", QVariant::fromValue(QString("plain")));
m_authMethodCombo->addItem("Autenticación OAuth2", QVariant::fromValue(QString("oauth2")));
form->addRow("Método:", m_authMethodCombo);
m_authPage->setLayout(form);
}
void ConnectionWizard::setupPageTest()
{
m_testPage = new QWizardPage(this);
m_testPage->setTitle("Probar conexión");
m_testPage->setSubTitle("Verifica que los datos ingresados sean correctos.");
QVBoxLayout *lay = new QVBoxLayout(m_testPage);
m_testStatusLabel = new QLabel("Listo para probar la conexión.");
m_testStatusLabel->setAlignment(Qt::AlignCenter);
m_testStatusLabel->setStyleSheet("font-size: 14px; color: #555;");
lay->addWidget(m_testStatusLabel);
m_testDetailLabel = new QLabel("");
m_testDetailLabel->setWordWrap(true);
m_testDetailLabel->setAlignment(Qt::AlignCenter);
m_testDetailLabel->setStyleSheet("font-size: 12px; color: #888;");
lay->addWidget(m_testDetailLabel);
lay->addStretch();
m_testButton = new QPushButton("Probar conexión ahora");
m_testButton->setStyleSheet(
"QPushButton { background-color: #0071e3; color: white; border: none; padding: 8px 16px; border-radius: 4px; }"
"QPushButton:hover { background-color: #005bb5; }"
);
lay->addWidget(m_testButton, 0, Qt::AlignCenter);
connect(m_testButton, &QPushButton::clicked, this, &ConnectionWizard::onTestClicked);
m_testPage->setLayout(lay);
}
void ConnectionWizard::onTestClicked()
{
if (!m_accountService) {
m_testStatusLabel->setText("⚠️ Servicio de cuenta no disponible.");
m_testStatusLabel->setStyleSheet("color: #e60000;");
m_testDetailLabel->setText("El servicio de cuenta no está inicializado.");
return;
}
Account::ConnectionSettings settings = connectionSettings();
// Basic validation
if (settings.incomingHost.isEmpty() || settings.username.isEmpty() || settings.password.isEmpty()) {
m_testStatusLabel->setText("⚠️ Faltan datos obligatorios.");
m_testStatusLabel->setStyleSheet("color: #e60000;");
m_testDetailLabel->setText("Por favor, completa los campos de servidor entrante, usuario y contraseña.");
return;
}
m_testStatusLabel->setText("⏳ Probando conexión...");
m_testStatusLabel->setStyleSheet("color: #0071e3;");
m_testDetailLabel->setText("Conectando al servidor IMAP...");
// Use a small delay to allow UI update before blocking call
QTimer::singleShot(0, this, [this, settings]() {
QString errorMsg;
bool ok = m_accountService->testConnection(settings, errorMsg);
if (ok) {
m_testStatusLabel->setText("✅ Conexión exitosa.");
m_testStatusLabel->setStyleSheet("color: #34c759;");
m_testDetailLabel->setText("Los datos ingresados son correctos. Puedes finalizar la configuración.");
} else {
m_testStatusLabel->setText("❌ Error de conexión.");
m_testStatusLabel->setStyleSheet("color: #ff3b30;");
m_testDetailLabel->setText(errorMsg.isEmpty() ? "Error desconocido." : errorMsg);
}
});
}
Account::ConnectionSettings ConnectionWizard::connectionSettings() const
{
Account::ConnectionSettings s;
// Incoming
s.type = m_incomingTypeCombo->currentData().toString(); // "imap" or "pop3"
s.incomingHost = m_incomingHostEdit->text().trimmed();
s.incomingPort = m_incomingPortEdit->text().toInt();
s.incomingSsl = m_incomingSslCheck->isChecked();
// Outgoing (SMTP)
s.outgoingHost = m_outgoingHostEdit->text().trimmed();
s.outgoingPort = m_outgoingPortEdit->text().toInt();
s.outgoingSsl = m_outgoingSslCheck->isChecked();
// Auth
s.username = m_usernameEdit->text().trimmed();
s.password = m_passwordEdit->text();
s.authMethod = m_authMethodCombo->currentData().toString(); // "plain" or "oauth2"
return s;
}
void ConnectionWizard::accept()
{
// When Finish is pressed, emit the settings and close
Account::ConnectionSettings settings = connectionSettings();
emit settingsReady(settings);
QWizard::accept();
}
#include "connectionwizard.moc"
+73
View File
@@ -0,0 +1,73 @@
#ifndef CONNECTIONWIZARD_H
#define CONNECTIONWIZARD_H
#include <QWizard>
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
#include <QIntValidator>
#include <QLabel>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QGroupBox>
#include <QRadioButton>
#include "core/models/account.h"
#include "services/accountservice.h"
class ConnectionWizard : public QWizard {
Q_OBJECT
public:
explicit ConnectionWizard(QWidget *parent = nullptr, AccountService *accountService = nullptr);
~ConnectionWizard() override = default;
// Retrieve the filled connection settings
Account::ConnectionSettings connectionSettings() const;
signals:
void settingsReady(const Account::ConnectionSettings &settings);
private slots:
void accept() override;
void onTestClicked();
private:
void setupPageIntro();
void setupPageIncoming();
void setupPageOutgoing();
void setupPageAuth();
void setupPageTest();
// Page pointers (optional)
QWizardPage *m_introPage;
QWizardPage *m_incomingPage;
QWizardPage *m_outgoingPage;
QWizardPage *m_authPage;
QWizardPage *m_testPage;
// Incoming fields
QComboBox *m_incomingTypeCombo; // IMAP, POP3
QLineEdit *m_incomingHostEdit;
QLineEdit *m_incomingPortEdit;
QCheckBox *m_incomingSslCheck;
// Outgoing fields
QLineEdit *m_outgoingHostEdit;
QLineEdit *m_outgoingPortEdit;
QCheckBox *m_outgoingSslCheck;
// Auth fields
QLineEdit *m_usernameEdit;
QLineEdit *m_passwordEdit;
QComboBox *m_authMethodCombo; // Plain, Login, OAuth2 (if supported)
// Test page
QLabel *m_testStatusLabel;
QLabel *m_testDetailLabel;
QPushButton *m_testButton;
AccountService *m_accountService;
};
#endif // CONNECTIONWIZARD_H
+109
View File
@@ -0,0 +1,109 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start of delete connect
start = None
for i, line in enumerate(lines):
if 'connect(deleteAction, &QAction::triggered' in line:
start = i
break
if start is None:
print('Could not find delete connect start')
sys.exit(1)
# Find end of that connect (the matching '});' after start)
end = None
brace_count = 0
for i in range(start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0 and '});' in line:
end = i
break
if end is not None:
break
if end is None:
print('Could not find end of delete connect')
sys.exit(1)
# Replacement delete lambda
new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
if (m_mailService->deleteMail(QString::number(id))) {
statusBar()->showMessage(tr("Email deleted"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to delete email"), 2000);
}
});\n'''
# Replace lines[start:end+1] with new_delete
lines = lines[:start] + [new_delete] + lines[end+1:]
# Now add flag action declaration after deleteAction declaration
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# Find where to insert flag connection: after the delete connect we just placed
# Search for the '});' that ends the new delete connect (starting from start)
insert_point = None
for i in range(start, len(lines)):
if '});' in lines[i]:
insert_point = i + 1
break
if insert_point is None:
insert_point = len(lines)
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:insert_point] + [flag_conn] + lines[insert_point:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Delete and flag actions updated')
+24
View File
@@ -0,0 +1,24 @@
// Trigger initial mail sync for existing accounts on startup
QVector<Account> accounts = AccountDao::findAll();
for (const Account &account : accounts) {
if (account.type() == AccountType::IMAP) {
QVector<Folder> folders = FolderDao::findByAccountId(account.id());
QString inboxFolderId;
for (const Folder &folder : folders) {
if (folder.isInbox()) {
inboxFolderId = QString::number(folder.id());
break;
}
}
if (inboxFolderId.isEmpty() && !folders.isEmpty()) {
// fallback to first folder
inboxFolderId = QString::number(folders.first().id());
}
if (!inboxFolderId.isEmpty()) {
qDebug() << "[MainMainWindow] Triggering initial mail sync for account" << account.email() << "folder" << inboxFolderId;
m_mailService->fetchMails(QString::number(account.id()), inboxFolderId);
} else {
qWarning() << "[MainMainWindow] No folders found for account" << account.id();
}
}
}
+206 -42
View File
@@ -15,8 +15,11 @@
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
qDebug() << "[MainMainWindow] Constructor start";
setupUI();
qDebug() << "[MainMainWindow] setupUI done";
connectModels();
qDebug() << "[MainMainWindow] connectModels done";
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
@@ -27,12 +30,20 @@ MainMainWindow::MainMainWindow(QWidget *parent)
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
connect(m_mailService, &MailService::mailSent, this, [this](const QString &) {
statusBar()->showMessage(tr("Message sent"), 5000);
});
connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) {
statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000);
});
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
qDebug() << "[MainMainWindow] Constructor complete";
}
void MainMainWindow::setupUI()
{
qDebug() << "[MainMainWindow::setupUI] Start";
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
@@ -40,8 +51,10 @@ void MainMainWindow::setupUI()
"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; }"
);
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
createToolBar();
qDebug() << "[MainMainWindow::setupUI] Toolbar created";
// === Central widget ===
QWidget *central = new QWidget();
@@ -51,6 +64,7 @@ void MainMainWindow::setupUI()
// === Sidebar ===
setupSidebar();
qDebug() << "[MainMainWindow::setupUI] Sidebar created";
centralLayout->addWidget(m_sidebar);
// === Separator line ===
@@ -62,23 +76,34 @@ void MainMainWindow::setupUI()
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
qDebug() << "[MainMainWindow::setupUI] Stack created";
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
qDebug() << "[MainMainWindow::setupUI] Mail page created";
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr, const QStringList &attachmentPaths) {
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
if (fromAddr.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddr, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
@@ -120,6 +145,7 @@ void MainMainWindow::setupUI()
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
qDebug() << "[MainMainWindow::setupUI] Settings page created";
// Page 3: Contacts
m_contactsView = new ContactsView();
@@ -131,9 +157,11 @@ void MainMainWindow::setupUI()
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
qDebug() << "[MainMainWindow::setupUI] Central widget set";
// Show mail page by default
switchToPage(PageMail);
qDebug() << "[MainMainWindow::setupUI] Done";
}
void MainMainWindow::setupSidebar()
@@ -161,8 +189,7 @@ void MainMainWindow::setupSidebar()
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
{ m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
@@ -189,7 +216,30 @@ void MainMainWindow::setupMailPage()
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
// Viewer stack: placeholder, reader, compose
m_viewerStack = new QStackedWidget();
// Placeholder widget
m_placeholderWidget = new QWidget();
QVBoxLayout *placeholderLayout = new QVBoxLayout(m_placeholderWidget);
placeholderLayout->setAlignment(Qt::AlignCenter);
QLabel *iconLabel = new QLabel();
QPixmap pixmap = QIcon::fromTheme("mail-unread").pixmap(48, 48);
if (pixmap.isNull()) {
pixmap = QPixmap(48, 48);
pixmap.fill(Qt::gray);
}
iconLabel->setPixmap(pixmap);
iconLabel->setAlignment(Qt::AlignCenter);
QLabel *textLabel = new QLabel(tr("Seleccione un correo para leerlo"));
textLabel->setAlignment(Qt::AlignCenter);
textLabel->setStyleSheet("color: #666; font-size: 14px;");
placeholderLayout->addStretch();
placeholderLayout->addWidget(iconLabel);
placeholderLayout->addWidget(textLabel);
placeholderLayout->addStretch();
m_placeholderWidget->setLayout(placeholderLayout);
// Reader view
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
@@ -198,9 +248,22 @@ void MainMainWindow::setupMailPage()
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
// Embedded compose view
m_embeddedComposeView = new ComposeView();
connect(m_embeddedComposeView, &ComposeView::sendRequested, this, &MainMainWindow::onEmbeddedSendRequested);
connect(m_embeddedComposeView, &ComposeView::discardRequested, this, &MainMainWindow::onEmbeddedDiscardRequested);
connect(m_embeddedComposeView, &ComposeView::detachRequested, this, &MainMainWindow::onEmbeddedDetachRequested);
// Add to stack
m_viewerStack->addWidget(m_placeholderWidget);
m_viewerStack->addWidget(m_emailViewer);
m_viewerStack->addWidget(m_embeddedComposeView);
m_viewerStack->setCurrentIndex(0); // show placeholder by default
m_folderSplitter->addWidget(m_viewerStack);
// Default sizes: folder 240, list 380, viewer flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
@@ -208,12 +271,18 @@ void MainMainWindow::setupMailPage()
void MainMainWindow::connectModels()
{
qDebug() << "[MainMainWindow::connectModels] Start";
m_accountService = new AccountService(this);
qDebug() << "[MainMainWindow::connectModels] AccountService created";
m_mailService = new MailService(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] MailService created";
m_composeView->setAccountService(m_accountService);
m_embeddedComposeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] FolderListModel created";
m_emailModel = new EmailListModel(this);
qDebug() << "[MainMainWindow::connectModels] EmailListModel created";
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
@@ -221,6 +290,18 @@ void MainMainWindow::connectModels()
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
connect(m_mailService, &MailService::mailFetched, this,
[this](const QString &, const QString &folderId, const QVector<MailItem> &) {
if (folderId.toInt() == m_currentFolderId) {
m_emailModel->refresh();
statusBar()->showMessage(tr("Mail synchronized"), 3000);
}
});
connect(m_mailService, &MailService::mailFetchError, this,
[this](const QString &, const QString &, const QString &error) {
statusBar()->showMessage(tr("Synchronization failed: %1").arg(error), 8000);
});
qDebug() << "[MainMainWindow::connectModels] Done";
}
void MainMainWindow::onNavChanged(int index)
@@ -239,8 +320,7 @@ void MainMainWindow::switchToPage(int pageIndex)
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
{ if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
@@ -257,43 +337,55 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
}
}
m_emailModel->refresh();
// Clear selection and show placeholder
m_currentMailId = -1;
m_mailListView->tableView()->clearSelection();
m_viewerStack->setCurrentIndex(0); // placeholder
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
{ m_currentMailId = mailId;
if (mailId >= 0) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
m_viewerStack->setCurrentIndex(0); // placeholder
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
m_viewerStack->setCurrentIndex(1); // reader
} else {
m_emailViewer->setMailItem(nullptr);
return;
m_viewerStack->setCurrentIndex(0); // placeholder
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
{ m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
{ if (item) {
m_embeddedComposeView->initializeComposition();
m_embeddedComposeView->setTo(item->sender());
m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject()));
// Optionally set body with quoted text? We'll leave empty for now.
m_embeddedComposeView->setBody(QString()); // clear
}
switchToPage(PageCompose);
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
{ // Show compose view in the viewer stack
m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
@@ -329,8 +421,11 @@ void MainMainWindow::createToolBar()
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
const auto folder = FolderDao::findById(m_currentFolderId);
if (folder) {
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
statusBar()->showMessage(tr("Synchronizing…"), 3000);
}
}
});
connect(openWinAction, &QAction::triggered, [this]() {
@@ -341,7 +436,12 @@ void MainMainWindow::createToolBar()
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
if (m_currentMailId >= 0) {
m_mailService->deleteMail(QString::number(m_currentMailId));
m_currentMailId = -1;
m_emailModel->refresh();
m_viewerStack->setCurrentIndex(0);
}
});
}
@@ -385,6 +485,70 @@ void MainMainWindow::onAccountEditRequested(int accountId)
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
m_accountService->removeAccount(accountId);
m_folderModel->refresh();
m_emailModel->setFolderId(-1);
m_emailModel->refresh();
m_currentFolderId = -1;
m_currentMailId = -1;
m_viewerStack->setCurrentIndex(0);
statusBar()->showMessage(tr("Account removed"), 3000);
}
void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress,
const QStringList &attachmentPaths)
{
if (scheduleTime.isValid()) {
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (fromAddress.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddress, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDiscardRequested()
{
// Go back to placeholder
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDetachRequested(QWidget *widget)
{
// Detach the compose view to a standalone window (similar to main compose view's detach)
QStackedWidget *stack = qobject_cast<QStackedWidget*>(widget->parentWidget());
if (stack) {
stack->removeWidget(widget);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(tr("Compose - Wino Mail"));
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(widget);
widget->setMinimumSize(800, 600);
widget->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage(tr("Compose view detached to separate window"), 3000);
}
+406
View File
@@ -0,0 +1,406 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString(\"Account added: %1\").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(\"Account not found\", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString(\"Account updated: %1\").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(\"Account not found\", 3000);
return;
}
if (QMessageBox::warning(this, \"Delete Account\",
QString(\"Are you sure you want to delete the account '%1'?\").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString(\"Account deleted: %1\").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+366
View File
@@ -0,0 +1,366 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+377
View File
@@ -0,0 +1,377 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
// Auto-select first folder to trigger mail fetch
if (m_folderModel->rowCount() > 0) {
QModelIndex firstIdx = m_folderModel->index(0, 0);
if (firstIdx.isValid()) {
int itemType = firstIdx.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_folderTree->setCurrentIndex(firstIdx);
onFolderSelected(firstIdx);
}
}
}
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+373
View File
@@ -0,0 +1,373 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QtConcurrentRun>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"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; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
m_settingsView->setAccountService(m_accountService);
connect(m_settingsView, &SettingsView::accountAddRequested, this, &MainMainWindow::onAddAccountRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"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; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector<MailItem> &items) {
int fid = folderId.toInt();
if (m_currentFolderId == fid || m_currentFolderId == -1) {
m_emailModel->refresh();
}
});
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QtConcurrent::run([this, accountId, folderId]() {
m_mailService->fetchMails(accountId, folderId);
});
delete account;
}
}
// Removed immediate refresh - will be updated via mailFetched signal
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+14 -3
View File
@@ -42,6 +42,14 @@ private slots:
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
// Slots for embedded compose view
void onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress,
const QStringList &attachmentPaths);
void onEmbeddedDiscardRequested();
void onEmbeddedDetachRequested(QWidget *widget);
private:
void setupUI();
void setupSidebar();
@@ -68,6 +76,9 @@ private:
QTreeView *m_folderTree;
MailListView *m_mailListView;
ReaderView *m_emailViewer;
QStackedWidget *m_viewerStack;
QWidget *m_placeholderWidget;
ComposeView *m_embeddedComposeView;
// Other pages
ComposeView *m_composeView;
@@ -78,12 +89,12 @@ private:
// Services & Models
FolderListModel *m_folderModel;
EmailListModel *m_emailModel;
AccountService *m_accountService;
MailService *m_mailService;
AccountService *m_accountService = nullptr;
MailService *m_mailService = nullptr;
QToolBar *m_toolBar;
int m_currentFolderId;
int m_currentMailId;
QProgressBar *m_progressBar;
};
};
View File
View File
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <QMainWindow>
#include <QStackedWidget>
#include <QListWidget>
#include <QSplitter>
#include <QTreeView>
#include <QStatusBar>
#include <QProgressBar>
#include <QToolBar>
#include <QAction>
#include "ui/readerview.h"
#include "ui/maillistview.h"
#include "ui/composeview.h"
#include "ui/settingsview.h"
#include "ui/contactsview.h"
#include "ui/calendarview.h"
#include "ui/models/FolderListModel.h"
#include "ui/models/EmailListModel.h"
#include "services/accountservice.h"
#include "services/mailservice.h"
class MainMainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainMainWindow(QWidget *parent = nullptr);
~MainMainWindow() override = default;
private slots:
void onNavChanged(int index);
void onFolderSelected(const QModelIndex &index);
void onEmailSelected(int mailId);
void onComposeRequested();
void onReaderReplyRequested(const MailItem *item);
void onNewMessage();
void openMailInIndependentWindow(int mailId);
void onAddAccountRequested();
void onAccountEditRequested(int accountId);
void onAccountDeleteRequested(int accountId);
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
private:
void setupUI();
void setupSidebar();
void setupMailPage();
void connectModels();
void createToolBar();
void switchToPage(int pageIndex);
// Navigation
QListWidget *m_sidebar;
QStackedWidget *m_stack;
enum Page {
PageMail = 0,
PageCompose,
PageSettings,
PageContacts,
PageCalendar
};
// Mail page widgets
QWidget *m_mailPage;
QSplitter *m_folderSplitter;
QTreeView *m_folderTree;
MailListView *m_mailListView;
ReaderView *m_emailViewer;
// Other pages
ComposeView *m_composeView;
SettingsView *m_settingsView;
ContactsView *m_contactsView;
CalendarView *m_calendarView;
// Services & Models
FolderListModel *m_folderModel;
EmailListModel *m_emailModel;
AccountService *m_accountService;
MailService *m_mailService;
QToolBar *m_toolBar;
int m_currentFolderId;
int m_currentMailId;
QProgressBar *m_progressBar;
};
+386
View File
@@ -0,0 +1,386 @@
import sys
import re
with open('composeview.cpp', 'r') as f:
lines = f.readlines()
# 1. Remove AddressInputWidget class definition (lines approx 27-52)
# Find start and end lines.
new_lines = []
i = 0
while i < len(lines):
if lines[i].strip() == '// Address input widget with tokenized entries, delete button, drag-drop reorder, and autocomplete':
# Skip until after the closing brace of the class
# Find the line with '};' after the class
j = i
while j < len(lines) and not lines[j].strip().startswith('};'):
j += 1
# include the '};' line
j += 1
i = j
continue
new_lines.append(lines[i])
i += 1
lines = new_lines
# 2. Replace member variable types
# We'll process line by line, replacing specific patterns.
out_lines = []
for line in lines:
# Replace member declarations
if 'AddressInputWidget *m_toWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_toWidget = nullptr;', 'QLineEdit *m_toField = nullptr;')
if 'AddressInputWidget *m_ccWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_ccWidget = nullptr;', 'QLineEdit *m_ccField = nullptr;')
if 'AddressInputWidget *m_bccWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_bccWidget = nullptr;', 'QLineEdit *m_bccField = nullptr;')
# Remove duplicate m_ccField and m_bccField lines (they are later)
if 'QLineEdit *m_ccField = nullptr; // kept for compatibility? actually not used now' in line:
# skip this line
continue
if 'QLineEdit *m_bccField = nullptr;' in line and '// kept for compatibility' not in line:
# This is the second duplicate; we already have one above; we can keep only one.
# We'll skip this line.
continue
out_lines.append(line)
lines = out_lines
# Now we need to modify setAccountService, setupUI, slots, etc.
# We'll do a more aggressive approach: replace entire sections using markers.
# Let's join back to string and use regex.
text = ''.join(lines)
# Replace setAccountService function
pattern_setAccountService = re.compile(r'void ComposeView::setAccountService\(AccountService \*service\)\s*\{[\s\S]*?\}', re.MULTILINE)
def repl_setAccountService(match):
return '''void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}'''
text = re.sub(pattern_setAccountService, repl_setAccountService, text)
# Replace setupCore function: we need to replace the whole function.
# We'll rewrite from scratch using a marker: we will replace the whole function body.
# Let's capture the function and replace.
pattern_setupUI = re.compile(r'void ComposeView::setupUI\(\)\s*\{[\s\S]*?\n\}\s*\n(?=void|\Z)', re.MULTILINE)
def repl_setupUI(match):
return '''void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr(\"From:\"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
\"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; \"
\"background: white; min-height: 20px; }\"\n
\"QComboBox:hover { border-color: #bdbdbd; }\"\n
\"QComboBox:focus { border-color: #1976D2; }\"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr(\"Detach compose window\"));
m_detachButton->setText(QStringLiteral(\"\"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
\"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \"
\"padding: 6px; color: #555; font-size: 14px; }\"\n
\"QPushButton:hover { background: #f0f0f0; }\"\n
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr(\"To:\"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr(\"To recipients\"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr(\"CC\"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr(\"BCC\"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr(\"Cc:\"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral(\"color: #555;\"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr(\"Cc recipients\"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr(\"Bcc:\"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral(\"color: #555;\"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr(\"Bcc recipients\"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr(\"Subject:\"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr(\"Subject\"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr(\"Send at:\"), this);
scheduleLabel->setStyleSheet(QStringLiteral(\"color: #555; font-weight: bold;\"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral(\"dd/MM/yyyy hh:mm AP\"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr(\"Schedule Send\"), this);
m_scheduleSendButton->setStyleSheet(
\"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \"
\"padding: 6px 16px; font-weight: bold; }\"\n
\"QPushButton:hover { background-color: #1565C0; }\"\n
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr(\"Discard\"), this);
m_discardButton->setStyleSheet(
\"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \"
\"padding: 8px 20px; color: #555; }\"\n
\"QPushButton:hover { background: #f5f5f5; }\"\n
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr(\"Send Now\"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr(\"Schedule for later...\"));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr(\"Send\"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
\"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \"
\"padding: 8px 24px; font-weight: bold; }\"\n
\"QToolButton:hover { background-color: #1565C0; }\"\n
\"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }\"\n
\"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }\"\n
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
'''
text = re.sub(pattern_setupUI, repl_setupUI, text)
# Replace onCcToggle slot (we renamed to onCcToggled with bool)
pattern_onCcToggle = re.compile(r'void ComposeView::onCcToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onCcToggle(match):
return '''void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
'''
text = re.sub(pattern_onCcToggle, repl_onCcToggle, text)
# Replace onBccToggle similarly
pattern_onBccToggle = re.compile(r'void ComposeView::onBccToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onBccToggle(match):
return '''void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
'''
text = re.sub(pattern_onBccToggle, repl_onBccToggle, text)
# Replace onSendClicked
pattern_onSendClicked = re.compile(r'void ComposeView::onSendClicked\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onSendClicked(match):
return '''void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
'''
text = re.sub(pattern_onSendClicked, repl_onSendClicked, text)
# Replace initializeComposition
pattern_initComp = re.compile(r'void ComposeView::initializeComposition\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_initComp(match):
return '''void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
'''
text = re.sub(pattern_initComp, repl_initComp, text)
# Replace startNewEmail
pattern_startNew = re.compile(r'void ComposeView::startNewEmail\(const QString \&initialRecipient\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_startNew(match):
return '''void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
'''
text = re.sub(pattern_startNew, repl_startNew, text)
# Replace setTo
pattern_setTo = re.compile(r'void ComposeView::setTo\(const QString \&to\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_setTo(match):
return '''void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression(\"[,;]\"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join(\"; \"));
}
'''
text = re.sub(pattern_setTo, repl_setTo, text)
# We also need to add a helper function splitAddresses if we used it, but we didn't.
# Ensure we have the forward declaration for the slot in header (already done).
# Now write the file.
with open('composeview.cpp', 'w') as f:
f.write(text)
+38
View File
@@ -0,0 +1,38 @@
import sys
filename = 'mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find the line after the mailFetchError connect (look for the closing brace and semicolon of that connect)
# We'll look for the pattern: '});' that ends the mailFetchError lambda.
insert_idx = None
for i, line in enumerate(lines):
if 'connect(m_mailService, &MailService::mailFetchError' in line:
# Find the closing brace of this call (could be same line or later)
# We'll just look for the next line that contains '});' after this line.
for j in range(i, len(lines)):
if '});' in lines[j]:
insert_idx = j + 1 # insert after this line
break
break
if insert_idx is None:
print("Could not find insert point")
sys.exit(1)
# The insertion block
insert_lines = [
' connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &mailItemId, const QString &error) {\n',
' qWarning() << \"[MailSendFailed]\" << error;\n',
' statusBar()->showMessage(tr(\"Error sending mail: %1\").arg(error), 5000);\n',
' });\n'
]
# Insert
lines = lines[:insert_idx] + insert_lines + lines[insert_idx:]
with open(filename, 'w') as f:
f.writelines(lines)
print(f"Inserted mailSendFailed connection at line {insert_idx+1}")
+25 -1
View File
@@ -1,5 +1,8 @@
#include "ui/readerview.h"
#include <QFont>
#include <QDesktopServices>
#include <QUrl>
#include "db/dao/mailitemdao.h"
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
setupUI();
@@ -55,8 +58,19 @@ void ReaderView::setupUI() {
m_bodyViewer->setOpenExternalLinks(true);
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
m_attachmentList = new QListWidget();
m_attachmentList->setVisible(false);
m_attachmentList->setMaximumHeight(110);
m_attachmentList->setToolTip(tr("Double-click an attachment to open it"));
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this,
[](QListWidgetItem *listItem) {
const QString path = listItem->data(Qt::UserRole).toString();
if (!path.isEmpty()) QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
mainLayout->addWidget(headerWidget);
mainLayout->addLayout(actionsLayout);
mainLayout->addWidget(m_attachmentList);
mainLayout->addWidget(m_bodyViewer);
// Connections
@@ -72,6 +86,8 @@ void ReaderView::setMailItem(const MailItem* item) {
m_subjectLabel->setText("No mail selected");
m_fromLabel->setText("From: ");
m_dateLabel->setText("Date: ");
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
return;
}
@@ -79,5 +95,13 @@ void ReaderView::setMailItem(const MailItem* item) {
m_subjectLabel->setText(item->subject());
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm")));
m_attachmentList->clear();
const QVector<StoredAttachmentRecord> attachments = MailItemDao::attachmentsForMail(item->id());
for (const StoredAttachmentRecord &attachment : attachments) {
auto *listItem = new QListWidgetItem(attachment.fileName, m_attachmentList);
listItem->setData(Qt::UserRole, attachment.storedPath);
listItem->setToolTip(attachment.storedPath);
}
m_attachmentList->setVisible(!attachments.isEmpty());
m_bodyViewer->setHtml(item->bodyHtml());
}
}
+2
View File
@@ -4,6 +4,7 @@
#include <QLabel>
#include <QTextBrowser>
#include <QPushButton>
#include <QListWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "core/mailitem.h"
@@ -31,6 +32,7 @@ private:
QLabel *m_fromLabel;
QLabel *m_dateLabel;
QTextBrowser *m_bodyViewer;
QListWidget *m_attachmentList;
QPushButton *m_replyButton;
QPushButton *m_forwardButton;
+50
View File
@@ -0,0 +1,50 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start line
start = None
for i, line in enumerate(lines):
if line.strip().startswith('connect(deleteAction, &QAction::triggered'):
start = i
break
if start is None:
print('Could not find deleteAction connect')
sys.exit(1)
# Find end line: look for a line that contains '});' after start, assuming it's the end of the lambda
end = None
for i in range(start, len(lines)):
if '});' in lines[i]:
end = i
break
if end is None:
print('Could not find end of lambda')
sys.exit(1)
# Replacement lines
new_lines = [
' connect(deleteAction, &QAction::triggered, [this]() {\n',
' QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();\n',
' if (indexes.isEmpty()) {\n',
' statusBar()->showMessage(tr(\"No email selected\"), 2000);\n',
' return;\n',
' }\n',
' int row = indexes.first().row();\n',
' QModelIndex idx = m_emailModel->index(row, 0);\n',
' qint64 id = idx.data(EmailListModel::IdRole).toLongLong();\n',
' m_mailService->deleteMail(QString::number(id)); // deletes from DB\n',
' m_emailModel->refresh();\n',
' statusBar()->showMessage(tr(\"Email deleted\"), 2000);\n',
' });\n'
]
# Replace
lines = lines[:start] + new_lines + lines[end+1:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Replace done')
+462
View File
@@ -0,0 +1,462 @@
#include "richtexteditor.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextImageFormat>
#include <QTextTableFormat>
#include <QTextLength>
#include <QPixmap>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QApplication>
#include <QMenu>
#include <QAction>
#include <QWidget>
#include <QLabel>
#include <QResizeEvent>
#include <QTextTableCell>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
setMouseTracking(true);
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\n"
"QToolButton { padding: 4px 6px; border-radius: 3px; }\n"
"QToolButton:hover { background: #e0e0e0; }\n"
"QToolButton:checked { background: #bbdefb; }\n"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(11);
m_fontSizeSpin->setFixedWidth(70);
m_fontSizeSpin->setFixedHeight(24);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
boldAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg")));
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
italicAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg")));
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
underlineAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg")));
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }\n"
"QToolButton:hover { background: #e0e0e0; }\n"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\n"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
/* ===================== Mouse events for image/table resizing ===================== */
void RichTextEditor::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
QPoint pos = event->pos();
QTextCursor cursor = cursorForPosition(pos);
// Image
if (cursor.charFormat().isImageFormat()) {
qDebug() << "Mouse press on image";
m_resizingImage = true;
m_resizeStartPos = pos;
QTextImageFormat imgFormat = cursor.charFormat().toImageFormat();
m_imageStartSize = QSize(imgFormat.width(), imgFormat.height());
if (m_imageStartSize.isEmpty()) {
QPixmap pm(imgFormat.name());
if (!pm.isNull())
m_imageStartSize = pm.size();
}
// Change cursor to indicate resizing
setCursor(Qt::SizeAllCursor);
event->accept();
return;
}
// Table
QTextTable *table = cursor.currentTable();
if (table) {
qDebug() << "Mouse press on table";
m_resizingTable = true;
m_tableStartPos = pos;
m_currentTable = table;
QTextTableFormat fmt = table->format();
m_tableStartSize = QSizeF(fmt.width().rawValue(), fmt.height().rawValue());
setCursor(Qt::SizeAllCursor);
event->accept();
return;
}
}
QTextEdit::mousePressEvent(event);
}
void RichTextEditor::mouseMoveEvent(QMouseEvent *event) {
if (m_resizingImage) {
qDebug() << "Mouse move resizing image";
QPoint delta = event->pos() - m_resizeStartPos;
int newWidth = qMax(20, m_imageStartSize.width() + delta.x());
int newHeight = qMax(20, m_imageStartSize.height() + delta.y());
QTextCursor cursor = cursorForPosition(m_resizeStartPos);
if (cursor.charFormat().isImageFormat()) {
QTextImageFormat fmt = cursor.charFormat().toImageFormat();
fmt.setWidth(newWidth);
fmt.setHeight(newHeight);
cursor.mergeCharFormat(fmt);
setTextCursor(cursor);
}
event->accept();
return;
}
if (m_resizingTable) {
qDebug() << "Mouse move resizing table";
QPoint delta = event->pos() - m_tableStartPos;
qreal newWidth = qMax(10.0, m_tableStartSize.width() + delta.x());
qreal newHeight = qMax(10.0, m_tableStartSize.height() + delta.y());
QTextTableFormat fmt = m_currentTable->format();
fmt.setWidth(QTextLength(QTextLength::FixedLength, newWidth));
fmt.setHeight(QTextLength(QTextLength::FixedLength, newHeight));
m_currentTable->setFormat(fmt);
event->accept();
return;
}
QTextEdit::mouseMoveEvent(event);
}
void RichTextEditor::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
if (m_resizingImage) {
qDebug() << "Mouse release image resize";
m_resizingImage = false;
setCursor(Qt::ArrowCursor);
event->accept();
return;
}
if (m_resizingTable) {
qDebug() << "Mouse release table resize";
m_resizingTable = false;
setCursor(Qt::ArrowCursor);
event->accept();
return;
}
}
QTextEdit::mouseReleaseEvent(event);
}
void RichTextEditor::contextMenuEvent(QContextMenuEvent *event) {
QPoint pos = event->pos();
QTextCursor cursor = cursorForPosition(pos);
QTextTable *table = cursor.currentTable();
if (table) {
showTableContextMenu(pos);
event->accept();
return;
}
QTextEdit::contextMenuEvent(event);
}
/* ===================== Helper functions ===================== */
void RichTextEditor::showTableContextMenu(const QPoint &pos) {
if (!m_tableContextMenu) {
m_tableContextMenu = new QMenu(this);
m_tableContextMenu->addAction(tr("Insert row above"), this, &RichTextEditor::addTableRow);
m_tableContextMenu->addAction(tr("Insert row below"), this, &RichTextEditor::addTableRow);
m_tableContextMenu->addAction(tr("Insert column left"), this, &RichTextEditor::addTableColumn);
m_tableContextMenu->addAction(tr("Insert column right"), this, &RichTextEditor::addTableColumn);
m_tableContextMenu->addSeparator();
m_tableContextMenu->addAction(tr("Delete selected rows"), this, &RichTextEditor::removeTableRow);
m_tableContextMenu->addAction(tr("Delete selected columns"), this, &RichTextEditor::removeTableColumn);
m_tableContextMenu->addSeparator();
m_tableContextMenu->addAction(tr("Border..."), this, &RichTextEditor::modifyTableBorder);
}
m_tableContextMenu->exec(mapToGlobal(pos));
}
void RichTextEditor::addTableRow() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int row = cell.row();
table->insertRows(row, 1);
}
void RichTextEditor::removeTableRow() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int row = cell.row();
table->removeRows(row, 1);
}
void RichTextEditor::addTableColumn() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int col = cell.column();
table->insertColumns(col, 1);
}
void RichTextEditor::removeTableColumn() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int col = cell.column();
table->removeColumns(col, 1);
}
void RichTextEditor::modifyTableBorder() {
if (!m_currentTable) return;
bool ok;
int border = QInputDialog::getInt(this, tr("Table Border"), tr("Border width (px):"),
m_currentTable->format().border(), 0, 20, 1, &ok);
if (!ok) return;
QTextTableFormat fmt = m_currentTable->format();
fmt.setBorder(border);
m_currentTable->setFormat(fmt);
}
+281
View File
@@ -0,0 +1,281 @@
#include "richtexteditor.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextImageFormat>
#include <QTextTableFormat>
#include <QTextLength>
#include <QPixmap>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText(\"Write your message here...\");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar(\"Formatting\");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
\"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\"\n
\"QToolButton { padding: 4px 6px; border-radius: 3px; }\"\n
\"QToolButton:hover { background: #e0e0e0; }\"\n
\"QToolButton:checked { background: #bbdefb; }\"\n
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(11);
m_fontSizeSpin->setFixedWidth(70);
m_fontSizeSpin->setFixedHeight(24);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic /
QAction *boldAct = m_toolbar->addAction(\"B\");
boldAct->setCheckable(true);
boldAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg\")));
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction(\"I\");
italicAct->setCheckable(true);
italicAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg\")));
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction(\"U\");
underlineAct->setCheckable(true);
underlineAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg\")));
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction(\"L\");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction(\"C\");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction(\"R\");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction(\"J\");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction(\"Bullets\");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction(\"1. List\");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction(\"Indent\");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction(\"Outdent\");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction(\"Img\");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction(\"Tbl\");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText(\"Signature\");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
\"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }\"\n
\"QToolButton:hover { background: #e0e0e0; }\"\n
\"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\"\n
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr(\"Editar firmas\"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, \"Insert Image\", QString(), \"Images (*.png *.jpg *.jpeg *.gif *.bmp)\");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, \"Table Rows\", \"Rows:\", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, \"Table Columns\", \"Columns:\", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef RICHTEXTEDITOR_H
#define RICHTEXTEDITOR_H
#include <QTextEdit>
#include <QToolBar>
#include <QFontComboBox>
#include <QSpinBox>
#include <QToolButton>
#include <QMenu>
#include <QVBoxLayout>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QPoint>
#include <QSize>
#include <QSizeF>
class QTextTable;
class QTextCursor;
class RichTextEditor : public QTextEdit
{
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
// Image resize handling
bool m_resizingImage = false;
QPoint m_resizeStartPos;
QSize m_imageStartSize;
// Table resize handling
bool m_resizingTable = false;
QPoint m_tableStartPos;
QTextTable *m_currentTable = nullptr;
QSizeF m_tableStartSize;
QMenu *m_tableContextMenu = nullptr;
// Helper functions
void showTableContextMenu(const QPoint &pos);
void addTableRow();
void removeTableRow();
void addTableColumn();
void removeTableColumn();
void modifyTableBorder();
};
#endif // RICHTEXTEDITOR_H
+116
View File
@@ -0,0 +1,116 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start of delete connect
start = None
for i, line in enumerate(lines):
if 'connect(deleteAction, &QAction::triggered' in line:
start = i
break
if start is None:
print('Could not find delete connect start')
sys.exit(1)
# Find end of that connect (the matching '});' after start)
end = None
brace_count = 0
for i in range(start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0 and '});' in line:
end = i
break
if end is not None:
break
if end is None:
print('Could not find end of delete connect')
sys.exit(1)
# Replacement delete lambda
new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
if (m_mailService->deleteMail(QString::number(id))) {
statusBar()->showMessage(tr("Email deleted"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to delete email"), 2000);
}
});\n'''
# Replace lines[start:end+1] with new_delete
lines = lines[:start] + [new_delete] + lines[end+1:]
# Now add flag action declaration after deleteAction declaration
# Find deleteAction declaration line
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# Find where to insert flag connection: after the delete connect we just placed (now at start)
# Actually after the delete connect (which now starts at 'start' and ends at start (since we replaced with one line?) Actually new_delete is multiple lines; we inserted as a single string but it contains newlines.
# We'll just insert flag connection after the delete connection block (i.e., after the line where we inserted new_delete).
# We'll search for the end of the new_delete block by looking for the line that contains '});' after start.
# But easier: insert after the deleteAction declaration line? No, we want to be after_decl_line.
# Let's just insert flag connection after the delete connection by adding it after the line that contains the end of delete connect.
# We'll search for the pattern '});' that ends the delete connect (should be in new_delete).
# We'll find the line index of that '});' after start.
for i in range(start, len(lines)):
if '});' in lines[i]:
insert_point = i + 1
break
else:
insert_point is None:
insert_point = len(lines)
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:insert_point] + [flag_conn] + lines[insert_point:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Delete and flag actions updated')
+240
View File
@@ -0,0 +1,240 @@
import sys
filename = 'mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# 1. Modify onFolderSelected function
# Find the start of onFolderSelected
on_folder_start = None
for i, line in enumerate(lines):
if line.strip().startswith('void MainMainWindow::onFolderSelected(const QModelIndex &index)'):
on_folder_start = i
break
if on_folder_start is None:
print('Could not find onFolderSelected')
sys.exit(1)
# Find the end of the function (look for a line that is just '}' after the function start)
# We'll assume the function ends at the next line that starts with 'void ' or '}' at indentation 0? Safer: find the matching brace.
brace_count = 0
in_function = False
on_folder_end = None
for i in range(on_folder_start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_function = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_function:
on_folder_end = i
break
if on_folder_end is not None:
break
if on_folder_end is None:
print('Could not find end of onFolderSelected')
sys.exit(1)
# Now we need to replace the content between on_folder_start+1 and on_folder_end with new implementation.
# Let's first extract the current function to see what we have.
# We'll replace from line after the opening brace? Actually we want to keep the signature and the opening brace.
# We'll replace lines from on_folder_start+1 to on_folder_end-1 with new body.
# But we need to keep the opening brace line (which is at on_folder_start? Actually the signature line is on_folder_start, the opening brace is on the same line or next?
# Look at the signature line: it ends with '{'? Let's check.
# Let's just replace the whole block from on_folder_start to on_folder_end with new function.
new_on_folder = '''void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
}
}
} else if (itemType == FolderTreeItem::AccountNode) {
// When an account node is selected, select its first folder (if any)
int childCount = m_folderModel->rowCount(index);
if (childCount > 0) {
QModelIndex firstChildIdx = m_folderModel->index(0, 0, index);
m_folderTree->setCurrentIndex(firstChildIdx);
onFolderSelected(firstChildIdx); // recursive call to handle folder selection
} else {
// No folders yet; clear email list
m_currentFolderId = -1;
m_emailModel->setFolderId(m_currentFolderId);
m_emailModel->refresh();
}
}
}
'''
# Replace lines
lines = lines[:on_folder_start] + [new_on_folder] + lines[on_folder_end+1:]
# 2. Modify syncAction slot
# Find the createToolBox function? Actually we need to find the connect(syncAction, ...) line.
# Let's search for 'connect(syncAction, &QAction::triggered'
sync_connect_line = None
for i, line in enumerate(lines):
if 'connect(syncAction, &QAction::triggered' in line:
sync_connect_line = i
break
if sync_connect_line is None:
print('Could not find syncAction connect')
sys.exit(1)
# Find the end of that lambda (the matching '});' after that line)
brace_count = 0
in_lambda = False
sync_end = None
for i in range(sync_connect_line, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_lambda = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_lambda:
# Check if the line contains '});' (the end of the lambda)
if '});' in line:
sync_end = i
break
if sync_end is not None:
break
if sync_end is None:
print('Could not find end of syncAction lambda')
sys.exit(1)
# Replace the lambda body with new implementation
new_sync_lambda = ''' connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
statusBar()->showMessage(tr(\"Syncing...\"), 0); // 0 means until cleared
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Disconnect previous connections to avoid multiple slots? We'll just call directly via queued connection.
QMetaObject::invokeMethod(m_mailService, \"fetchMails\", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
}
}
}
});'''
# Replace lines from sync_connect_line to sync_end inclusive
lines = lines[:sync_connect_line] + [new_sync_lambda] + lines[sync_end+1:]
# 3. Update mailFetched and mailFetchError lambdas in connectModels
# Find the connectModels function
connect_models_start = None
for i, line in enumerate(lines):
if line.strip().startswith('void MainMainWindow::connectModels()'):
connect_models_start = i
break
if connect_models_start is None:
print('Could not find connectModels')
sys.exit(1)
# Find end of connectModels
brace_count = 0
in_func = False
connect_models_end = None
for i in range(connect_models_start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_func = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_func:
connect_models_end = i
break
if connect_models_end is not None:
break
if connect_models_end is None:
print('Could not find end of connectModels')
sys.exit(1)
# Within this function, we need to find the two lambdas: mailFetched and mailFetchError.
# We'll replace the whole function with a new version? That's risky.
# Instead, we'll replace the specific lambda bodies.
# Let's find the line numbers for the mailFetched connect and mailFetchError connect.
mail_fetched_line = None
mail_fetch_error_line = None
for i in range(connect_models_start, connect_models_end+1):
if 'connect(m_mailService, &MailService::mailFetched' in lines[i]:
mail_fetched_line = i
if 'connect(m_mailService, &MailService::mailFetchError' in lines[i]:
mail_fetch_error_line = i
# For each, find the end of the lambda (the '});' line)
def find_lambda_end(start_line):
brace_count = 0
in_lambda = False
for i in range(start_line, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_lambda = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_lambda:
# Check if line ends with '});'
if '});' in line:
return i
return None
mfe_end = None
mfer_end = None
if mail_fetched_line is not None:
mfe_end = find_lambda_end(mail_fetched_line)
if mail_fetch_error_line is not None:
mfer_end = find_lambda_end(mail_fetch_error_line)
if mfe_end is None or mfer_end is None:
print('Could not find lambda ends')
sys.exit(1)
# New lambda bodies
new_mfe_lambda = ''' connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector<MailItem> &items) {
int fid = folderId.toInt();
if (m_currentFolderId == fid || m_currentFolderId == -1) {
m_emailModel->refresh();
}
statusBar()->showMessage(tr(\"Synced %1 message(s)\").arg(items.size()), 3000);
});'''
new_mfer_lambda = ''' connect(m_mailService, &MailService::mailFetchError, this, [this](const QString &accountId, const QString &folderId, const QString &error) {
qWarning() << \"[MailFetchError]\" << error;
statusBar()->showMessage(tr(\"Error fetching mail: %1\").arg(error), 5000);
});'''
# Replace the lambdas
lines = lines[:mail_fetched_line] + [new_mfe_lambda] + lines[mfe_end+1:mail_fetch_error_line] + [new_mfer_lambda] + lines[mfer_end+1:]
# Write back
with open(filename, 'w') as f:
f.writelines(lines)
print('Updated mainmainwindow.cpp')