#include "ui/composeview.h" #include #include #include #include #include #include #include #include // ===================== 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::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 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::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"