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:
@@ -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() ? "Sí" : "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 ? "Sí" : "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 ───────────────────
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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')
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user