407 lines
15 KiB
C++
407 lines
15 KiB
C++
#include "newmessagedialog.h"
|
|||
|
|
#include <QVBoxLayout>
|
||
|
|
#include <QHBoxLayout>
|
||
|
|
#include <QFormLayout>
|
||
|
|
#include <QMessageBox>
|
||
|
|
#include <QFileDialog>
|
||
|
|
#include <QDateTime>
|
||
|
|
#include <QTextList>
|
||
|
|
#include <QTextTable>
|
||
|
|
#include <QFileInfo>
|
||
|
|
#include <QCloseEvent>
|
||
|
|
|
||
|
|
NewMessageDialog::NewMessageDialog(const QStringList &accounts, QWidget *parent)
|
||
|
|
: QDialog(parent)
|
||
|
|
{
|
||
|
|
setWindowTitle("New Message");
|
||
|
|
setMinimumSize(750, 600);
|
||
|
|
resize(850, 700);
|
||
|
|
|
||
|
|
setupUI();
|
||
|
|
|
||
|
|
// Init state
|
||
|
|
m_ccEdit->hide();
|
||
|
|
m_bccEdit->hide();
|
||
|
|
m_schedulePicker->hide();
|
||
|
|
|
||
|
|
// Capture initial content for dirty detection
|
||
|
|
m_initialContent = m_bodyEdit->toPlainText();
|
||
|
|
|
||
|
|
// Signals
|
||
|
|
connect(m_sendNowBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendNow);
|
||
|
|
connect(m_sendLaterBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendLater);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::setupUI()
|
||
|
|
{
|
||
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||
|
|
mainLayout->setContentsMargins(16, 12, 16, 12);
|
||
|
|
mainLayout->setSpacing(8);
|
||
|
|
|
||
|
|
// ===== HEADER FIELDS =====
|
||
|
|
QVBoxLayout *headerLayout = new QVBoxLayout();
|
||
|
|
headerLayout->setSpacing(6);
|
||
|
|
|
||
|
|
// From
|
||
|
|
QHBoxLayout *fromRow = new QHBoxLayout();
|
||
|
|
m_fromCombo = new QComboBox(this);
|
||
|
|
m_fromCombo->setMinimumWidth(400);
|
||
|
|
fromRow->addWidget(new QLabel("From:", this));
|
||
|
|
fromRow->addWidget(m_fromCombo, 1);
|
||
|
|
headerLayout->addLayout(fromRow);
|
||
|
|
|
||
|
|
// To + CC/CCO buttons
|
||
|
|
QHBoxLayout *toRow = new QHBoxLayout();
|
||
|
|
m_toEdit = new QLineEdit(this);
|
||
|
|
m_toEdit->setPlaceholderText("To");
|
||
|
|
toRow->addWidget(new QLabel("To:", this));
|
||
|
|
toRow->addWidget(m_toEdit, 1);
|
||
|
|
m_ccBtn = new QPushButton("CC", this);
|
||
|
|
m_ccBtn->setFixedWidth(48);
|
||
|
|
m_ccBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||
|
|
m_bccBtn = new QPushButton("BCC", this);
|
||
|
|
m_bccBtn->setFixedWidth(48);
|
||
|
|
m_bccBtn->setStyleSheet(m_ccBtn->styleSheet());
|
||
|
|
toRow->addWidget(m_ccBtn);
|
||
|
|
toRow->addWidget(m_bccBtn);
|
||
|
|
headerLayout->addLayout(toRow);
|
||
|
|
|
||
|
|
connect(m_ccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleCc);
|
||
|
|
connect(m_bccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleBcc);
|
||
|
|
|
||
|
|
// CC
|
||
|
|
m_ccEdit = new QLineEdit(this);
|
||
|
|
m_ccEdit->setPlaceholderText("Cc");
|
||
|
|
QHBoxLayout *ccRow = new QHBoxLayout();
|
||
|
|
ccRow->addWidget(new QLabel("Cc:", this));
|
||
|
|
ccRow->addWidget(m_ccEdit, 1);
|
||
|
|
headerLayout->addLayout(ccRow);
|
||
|
|
|
||
|
|
// BCC
|
||
|
|
m_bccEdit = new QLineEdit(this);
|
||
|
|
m_bccEdit->setPlaceholderText("Bcc");
|
||
|
|
QHBoxLayout *bccRow = new QHBoxLayout();
|
||
|
|
bccRow->addWidget(new QLabel("Bcc:", this));
|
||
|
|
bccRow->addWidget(m_bccEdit, 1);
|
||
|
|
headerLayout->addLayout(bccRow);
|
||
|
|
|
||
|
|
// Subject
|
||
|
|
QHBoxLayout *subjRow = new QHBoxLayout();
|
||
|
|
m_subjectEdit = new QLineEdit(this);
|
||
|
|
m_subjectEdit->setPlaceholderText("Subject");
|
||
|
|
subjRow->addWidget(new QLabel("Subject:", this));
|
||
|
|
subjRow->addWidget(m_subjectEdit, 1);
|
||
|
|
headerLayout->addLayout(subjRow);
|
||
|
|
|
||
|
|
mainLayout->addLayout(headerLayout);
|
||
|
|
|
||
|
|
// ===== RICH TEXT TOOLBAR =====
|
||
|
|
m_richToolbar = new QToolBar("Format", this);
|
||
|
|
m_richToolbar->setIconSize(QSize(16, 16));
|
||
|
|
m_richToolbar->setStyleSheet(
|
||
|
|
"QToolBar { border: 1px solid #d1d1d6; border-radius: 4px; background: #f9f9f9; spacing: 4px; padding: 2px; }"
|
||
|
|
);
|
||
|
|
setupRichTextToolbar(m_richToolbar);
|
||
|
|
mainLayout->addWidget(m_richToolbar);
|
||
|
|
|
||
|
|
// ===== BODY EDITOR =====
|
||
|
|
m_bodyEdit = new QTextEdit(this);
|
||
|
|
m_bodyEdit->setAcceptRichText(true);
|
||
|
|
m_bodyEdit->setPlaceholderText("Write your message...");
|
||
|
|
m_bodyEdit->setFrameShape(QFrame::StyledPanel);
|
||
|
|
m_bodyEdit->setStyleSheet(
|
||
|
|
"QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-size: 13px; font-family: 'Segoe UI', Roboto, Helvetica; }"
|
||
|
|
"QTextEdit:focus { border-color: #0078d4; }"
|
||
|
|
);
|
||
|
|
mainLayout->addWidget(m_bodyEdit, 1);
|
||
|
|
|
||
|
|
connect(m_bodyEdit, &QTextEdit::currentCharFormatChanged, this, &NewMessageDialog::currentCharFormatChanged);
|
||
|
|
|
||
|
|
// ===== ATTACHMENTS =====
|
||
|
|
QHBoxLayout *attachRow = new QHBoxLayout();
|
||
|
|
m_attachBtn = new QPushButton("Attach Files...", this);
|
||
|
|
m_attachBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 12px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||
|
|
m_attachmentList = new QListWidget(this);
|
||
|
|
m_attachmentList->setMaximumHeight(60);
|
||
|
|
m_attachmentList->setMinimumWidth(300);
|
||
|
|
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #e0e0e0; border-radius: 4px; font-size: 11px; }");
|
||
|
|
m_attachmentList->setVisible(false);
|
||
|
|
attachRow->addWidget(m_attachBtn);
|
||
|
|
attachRow->addWidget(m_attachmentList, 1);
|
||
|
|
mainLayout->addLayout(attachRow);
|
||
|
|
|
||
|
|
connect(m_attachBtn, &QPushButton::clicked, this, &NewMessageDialog::onAttachFile);
|
||
|
|
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this, &NewMessageDialog::removeAttachment);
|
||
|
|
|
||
|
|
// ===== BOTTOM ACTION BAR =====
|
||
|
|
QHBoxLayout *actionBar = new QHBoxLayout();
|
||
|
|
|
||
|
|
// Schedule picker
|
||
|
|
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
|
||
|
|
m_schedulePicker->setCalendarPopup(true);
|
||
|
|
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
|
||
|
|
m_schedulePicker->setStyleSheet("QDateTimeEdit { border: 1px solid #ccc; border-radius: 4px; padding: 6px 8px; font-size: 12px; }");
|
||
|
|
|
||
|
|
actionBar->addStretch();
|
||
|
|
|
||
|
|
m_cancelBtn = new QPushButton("Discard", this);
|
||
|
|
m_cancelBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #f0f0f0; color: #333; } QPushButton:hover { background: #e0e0e0; }");
|
||
|
|
|
||
|
|
m_sendLaterBtn = new QPushButton("Schedule...", this);
|
||
|
|
m_sendLaterBtn->setStyleSheet("QPushButton { border: 1px solid #0078d4; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #ffffff; color: #0078d4; } QPushButton:hover { background: #e8f4ff; }");
|
||
|
|
|
||
|
|
m_sendNowBtn = new QPushButton("Send Now", this);
|
||
|
|
m_sendNowBtn->setStyleSheet("QPushButton { border: none; border-radius: 6px; padding: 8px 24px; font-size: 13px; font-weight: bold; background: #0078d4; color: white; } QPushButton:hover { background: #106ebe; }");
|
||
|
|
|
||
|
|
actionBar->addWidget(m_cancelBtn);
|
||
|
|
actionBar->addWidget(m_sendLaterBtn);
|
||
|
|
actionBar->addWidget(m_sendNowBtn);
|
||
|
|
mainLayout->addLayout(actionBar);
|
||
|
|
|
||
|
|
connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::setupRichTextToolbar(QToolBar *tb)
|
||
|
|
{
|
||
|
|
m_boldAction = tb->addAction("B");
|
||
|
|
m_boldAction->setCheckable(true);
|
||
|
|
m_boldAction->setToolTip("Bold (Ctrl+B)");
|
||
|
|
m_boldAction->setFont(QFont("Segoe UI", 11, QFont::Bold));
|
||
|
|
|
||
|
|
m_italicAction = tb->addAction("I");
|
||
|
|
m_italicAction->setCheckable(true);
|
||
|
|
m_italicAction->setToolTip("Italic (Ctrl+I)");
|
||
|
|
m_italicAction->setFont(QFont("Segoe UI", 11, QFont::StyleItalic));
|
||
|
|
|
||
|
|
m_underlineAction = tb->addAction("U");
|
||
|
|
m_underlineAction->setCheckable(true);
|
||
|
|
m_underlineAction->setToolTip("Underline (Ctrl+U)");
|
||
|
|
QFont uFont("Segoe UI", 11);
|
||
|
|
uFont.setUnderline(true);
|
||
|
|
m_underlineAction->setFont(uFont);
|
||
|
|
|
||
|
|
tb->addSeparator();
|
||
|
|
|
||
|
|
QAction *bulletAction = tb->addAction("• List");
|
||
|
|
bulletAction->setToolTip("Bullet list");
|
||
|
|
|
||
|
|
QAction *numAction = tb->addAction("1. List");
|
||
|
|
numAction->setToolTip("Numbered list");
|
||
|
|
|
||
|
|
tb->addSeparator();
|
||
|
|
|
||
|
|
QAction *tableAction = tb->addAction("▦ Table");
|
||
|
|
tableAction->setToolTip("Insert table");
|
||
|
|
|
||
|
|
QAction *imageAction = tb->addAction("🖼 Image");
|
||
|
|
imageAction->setToolTip("Insert image");
|
||
|
|
|
||
|
|
connect(m_boldAction, &QAction::triggered, this, &NewMessageDialog::onFormatBold);
|
||
|
|
connect(m_italicAction, &QAction::triggered, this, &NewMessageDialog::onFormatItalic);
|
||
|
|
connect(m_underlineAction, &QAction::triggered, this, &NewMessageDialog::onFormatUnderline);
|
||
|
|
connect(bulletAction, &QAction::triggered, this, &NewMessageDialog::onInsertBulletList);
|
||
|
|
connect(numAction, &QAction::triggered, this, &NewMessageDialog::onInsertNumberedList);
|
||
|
|
connect(tableAction, &QAction::triggered, this, &NewMessageDialog::onInsertTable);
|
||
|
|
connect(imageAction, &QAction::triggered, this, &NewMessageDialog::onInsertImage);
|
||
|
|
|
||
|
|
tb->addSeparator();
|
||
|
|
QAction *undoAction = tb->addAction("↶");
|
||
|
|
undoAction->setToolTip("Undo");
|
||
|
|
connect(undoAction, &QAction::triggered, m_bodyEdit, &QTextEdit::undo);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== FORMAT SLOTS =====
|
||
|
|
|
||
|
|
void NewMessageDialog::mergeFormatOnWordOrSelection(const QTextCharFormat &fmt)
|
||
|
|
{
|
||
|
|
QTextCursor cursor = m_bodyEdit->textCursor();
|
||
|
|
if (!cursor.hasSelection())
|
||
|
|
cursor.select(QTextCursor::WordUnderCursor);
|
||
|
|
cursor.mergeCharFormat(fmt);
|
||
|
|
m_bodyEdit->mergeCurrentCharFormat(fmt);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::currentCharFormatChanged(const QTextCharFormat &fmt)
|
||
|
|
{
|
||
|
|
m_boldAction->setChecked(fmt.fontWeight() >= QFont::Bold);
|
||
|
|
m_italicAction->setChecked(fmt.fontItalic());
|
||
|
|
m_underlineAction->setChecked(fmt.fontUnderline());
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onFormatBold()
|
||
|
|
{
|
||
|
|
QTextCharFormat fmt;
|
||
|
|
fmt.setFontWeight(m_boldAction->isChecked() ? QFont::Bold : QFont::Normal);
|
||
|
|
mergeFormatOnWordOrSelection(fmt);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onFormatItalic()
|
||
|
|
{
|
||
|
|
QTextCharFormat fmt;
|
||
|
|
fmt.setFontItalic(m_italicAction->isChecked());
|
||
|
|
mergeFormatOnWordOrSelection(fmt);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onFormatUnderline()
|
||
|
|
{
|
||
|
|
QTextCharFormat fmt;
|
||
|
|
fmt.setFontUnderline(m_underlineAction->isChecked());
|
||
|
|
mergeFormatOnWordOrSelection(fmt);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onInsertBulletList()
|
||
|
|
{
|
||
|
|
QTextCursor cursor = m_bodyEdit->textCursor();
|
||
|
|
cursor.insertList(QTextListFormat::ListDisc);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onInsertNumberedList()
|
||
|
|
{
|
||
|
|
QTextCursor cursor = m_bodyEdit->textCursor();
|
||
|
|
cursor.insertList(QTextListFormat::ListDecimal);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onInsertTable()
|
||
|
|
{
|
||
|
|
QTextCursor cursor = m_bodyEdit->textCursor();
|
||
|
|
QTextTable *table = cursor.insertTable(3, 3);
|
||
|
|
Q_UNUSED(table);
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onInsertImage()
|
||
|
|
{
|
||
|
|
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(),
|
||
|
|
"Images (*.png *.jpg *.jpeg *.gif *.bmp)");
|
||
|
|
if (filePath.isEmpty()) return;
|
||
|
|
|
||
|
|
QImage image(filePath);
|
||
|
|
if (image.isNull()) return;
|
||
|
|
|
||
|
|
// Scale large images to fit
|
||
|
|
if (image.width() > 600)
|
||
|
|
image = image.scaledToWidth(600, Qt::SmoothTransformation);
|
||
|
|
|
||
|
|
QTextCursor cursor = m_bodyEdit->textCursor();
|
||
|
|
cursor.insertImage(image);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== CC / BCC TOGGLES =====
|
||
|
|
|
||
|
|
void NewMessageDialog::toggleCc()
|
||
|
|
{
|
||
|
|
m_ccVisible = !m_ccVisible;
|
||
|
|
m_ccEdit->setVisible(m_ccVisible);
|
||
|
|
m_ccBtn->setStyleSheet(m_ccVisible
|
||
|
|
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
|
||
|
|
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::toggleBcc()
|
||
|
|
{
|
||
|
|
m_bccVisible = !m_bccVisible;
|
||
|
|
m_bccEdit->setVisible(m_bccVisible);
|
||
|
|
m_bccBtn->setStyleSheet(m_bccVisible
|
||
|
|
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
|
||
|
|
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== SEND =====
|
||
|
|
|
||
|
|
void NewMessageDialog::onSendNow()
|
||
|
|
{
|
||
|
|
if (m_toEdit->text().trimmed().isEmpty()) {
|
||
|
|
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
|
||
|
|
m_toEdit->setFocus();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_sendLater = false;
|
||
|
|
accept();
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::onSendLater()
|
||
|
|
{
|
||
|
|
if (m_toEdit->text().trimmed().isEmpty()) {
|
||
|
|
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
|
||
|
|
m_toEdit->setFocus();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (m_schedulePicker->dateTime() <= QDateTime::currentDateTime()) {
|
||
|
|
QMessageBox::warning(this, "Invalid Time", "Scheduled time must be in the future.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_sendLater = true;
|
||
|
|
m_scheduledTime = m_schedulePicker->dateTime();
|
||
|
|
accept();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== ATTACHMENTS =====
|
||
|
|
|
||
|
|
void NewMessageDialog::onAttachFile()
|
||
|
|
{
|
||
|
|
QStringList files = QFileDialog::getOpenFileNames(this, "Attach Files");
|
||
|
|
if (files.isEmpty()) return;
|
||
|
|
|
||
|
|
for (const QString &file : files) {
|
||
|
|
QFileInfo fi(file);
|
||
|
|
m_attachedFiles << file;
|
||
|
|
m_attachmentList->addItem(fi.fileName());
|
||
|
|
}
|
||
|
|
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::removeAttachment()
|
||
|
|
{
|
||
|
|
QListWidgetItem *item = m_attachmentList->currentItem();
|
||
|
|
if (!item) return;
|
||
|
|
|
||
|
|
int row = m_attachmentList->row(item);
|
||
|
|
m_attachedFiles.removeAt(row);
|
||
|
|
delete m_attachmentList->takeItem(row);
|
||
|
|
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== CLOSE WITH UNSAVED CHANGES =====
|
||
|
|
|
||
|
|
bool NewMessageDialog::hasUnsavedChanges() const
|
||
|
|
{
|
||
|
|
// Check if anything was typed
|
||
|
|
if (!m_toEdit->text().trimmed().isEmpty()) return true;
|
||
|
|
if (!m_subjectEdit->text().trimmed().isEmpty()) return true;
|
||
|
|
if (m_bodyEdit->toPlainText() != m_initialContent &&
|
||
|
|
!m_bodyEdit->toPlainText().trimmed().isEmpty()) return true;
|
||
|
|
if (!m_attachedFiles.isEmpty()) return true;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void NewMessageDialog::closeEvent(QCloseEvent *event)
|
||
|
|
{
|
||
|
|
if (hasUnsavedChanges()) {
|
||
|
|
QMessageBox::StandardButton reply = QMessageBox::question(
|
||
|
|
this, "Unsaved Changes",
|
||
|
|
"You have unsaved changes. Would you like to save to drafts before closing?",
|
||
|
|
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
||
|
|
QMessageBox::Save);
|
||
|
|
|
||
|
|
if (reply == QMessageBox::Save) {
|
||
|
|
// Accept the dialog so the caller can handle saving to drafts
|
||
|
|
accept();
|
||
|
|
return;
|
||
|
|
} else if (reply == QMessageBox::Cancel) {
|
||
|
|
event->ignore();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Discard: fall through to reject
|
||
|
|
}
|
||
|
|
event->accept();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== GETTERS =====
|
||
|
|
|
||
|
|
QString NewMessageDialog::fromAccount() const { return m_fromCombo->currentText(); }
|
||
|
|
QString NewMessageDialog::to() const { return m_toEdit->text().trimmed(); }
|
||
|
|
QString NewMessageDialog::cc() const { return m_ccEdit->text().trimmed(); }
|
||
|
|
QString NewMessageDialog::bcc() const { return m_bccEdit->text().trimmed(); }
|
||
|
|
QString NewMessageDialog::subject() const { return m_subjectEdit->text().trimmed(); }
|
||
|
|
QString NewMessageDialog::body() const { return m_bodyEdit->toHtml(); }
|