#include "mainmainwindow.h" #include "core/models/account.h" #include "core/mailitem.h" #include "db/dao/mailitemdao.h" #include "db/dao/folderdao.h" #include "db/dao/categorydao.h" #include "db/dao/ruledao.h" #include "services/rulesengine.h" #include "ui/rulesmanagerdialog.h" #include "ui/accountsetupdialog.h" #include "ui/delegates/FolderTreeDelegate.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif MainMainWindow::MainMainWindow(QWidget *parent) : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) { qDebug() << "[MainMainWindow] Constructor start"; // Enable mouse tracking for resize cursor setMouseTracking(true); setupUI(); qDebug() << "[MainMainWindow] setupUI done"; connectModels(); qDebug() << "[MainMainWindow] connectModels done"; setWindowTitle("Wino Mail DTK"); resize(1280, 820); // Note: status bar is removed in createTitleBar(), no progress bar in status bar // 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); // no status bar }); connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) { // statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000); // no status bar }); // 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; }" "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; }" ); qDebug() << "[MainMainWindow::setupUI] Stylesheet set"; createTitleBar(); qDebug() << "[MainMainWindow::setupUI] Title bar created"; // === Central widget === QWidget *central = new QWidget(); central->setObjectName("CentralWidget"); central->setStyleSheet( "QWidget#CentralWidget {" " background-color: #f5f5f7;" " border-bottom-left-radius: 8px;" " border-bottom-right-radius: 8px;" "}" ); // Install event filter to handle resize from window edges central->installEventFilter(this); QHBoxLayout *centralLayout = new QHBoxLayout(central); centralLayout->setContentsMargins(0, 0, 0, 0); centralLayout->setSpacing(0); // === Sidebar === setupSidebar(); qDebug() << "[MainMainWindow::setupUI] Sidebar created"; 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;"); 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, const QStringList &attachmentPaths) { if (scheduleTime.isValid()) { statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000); return; } 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]() { switchToPage(PageMail); }); connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { // Detach compose view to a standalone window QStackedWidget *stack = qobject_cast(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); qDebug() << "[MainMainWindow::setupUI] Settings page created"; // 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); qDebug() << "[MainMainWindow::setupUI] Central widget set"; // Show mail page by default switchToPage(PageMail); qDebug() << "[MainMainWindow::setupUI] Done"; } 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); // Main splitter with 3 panes: folders | categories | mail list | viewer m_folderSplitter = new QSplitter(Qt::Horizontal); m_folderSplitter->setHandleWidth(1); // Left pane: Folder tree + Category tree (vertical splitter) QSplitter* leftSplitter = new QSplitter(Qt::Vertical); leftSplitter->setHandleWidth(1); leftSplitter->setChildrenCollapsible(false); // Folder tree m_folderTree = new QTreeView(); m_folderTree->setHeaderHidden(true); m_folderTree->setIndentation(20); m_folderTree->setMinimumWidth(220); m_folderTree->setMaximumWidth(350); m_folderTree->setFrameShape(QFrame::NoFrame); m_folderTree->setExpandsOnDoubleClick(true); m_folderTree->setItemDelegate(new FolderTreeDelegate(this)); // Custom delegate with unread count badges leftSplitter->addWidget(m_folderTree); // Category tree m_categoryTree = new CategoryTreeWidget(); m_categoryTree->setMinimumHeight(200); m_categoryTree->setMaximumHeight(400); connect(m_categoryTree, &CategoryTreeWidget::categorySelected, this, &MainMainWindow::onCategorySelected); connect(m_categoryTree, &CategoryTreeWidget::categoryDoubleClicked, this, &MainMainWindow::onCategoryDoubleClicked); connect(m_categoryTree, &CategoryTreeWidget::assignCategoryRequested, this, &MainMainWindow::onAssignCategoryRequested); connect(m_categoryTree, &CategoryTreeWidget::categoryCreated, this, &MainMainWindow::onCategoryCreated); connect(m_categoryTree, &CategoryTreeWidget::categoryEdited, this, &MainMainWindow::onCategoryEdited); connect(m_categoryTree, &CategoryTreeWidget::categoryDeleted, this, &MainMainWindow::onCategoryDeleted); leftSplitter->addWidget(m_categoryTree); // Set initial sizes for left splitter: folder tree gets 60%, categories 40% leftSplitter->setSizes({400, 250}); leftSplitter->setStretchFactor(0, 3); leftSplitter->setStretchFactor(1, 2); m_folderSplitter->addWidget(leftSplitter); // Mail list (QTableView with sorting) 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); }); connect(m_mailListView, &MailListView::flagRequested, this, &MainMainWindow::onFlagRequested); connect(m_mailListView, &MailListView::deleteRequested, this, &MainMainWindow::onDeleteRequested); connect(m_mailListView, &MailListView::categoryRequested, this, &MainMainWindow::onCategoryRequested); connect(m_mailListView, &MailListView::moreRequested, this, &MainMainWindow::onMoreRequested); m_folderSplitter->addWidget(m_mailListView); // 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); connect(m_emailViewer, &ReaderView::forwardRequested, this, &MainMainWindow::onReaderForwardRequested); connect(m_emailViewer, &ReaderView::deleteRequested, this, &MainMainWindow::onReaderDeleteRequested); connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { if (m_currentMailId >= 0) { openMailInIndependentWindow(m_currentMailId); } }); // 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); connect(m_embeddedComposeView, &ComposeView::statusMessageRequested, this, [this](const QString& msg) { statusBar()->showMessage(msg, 3000); }); // 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: left pane (folders+categories) 300, list 380, viewer flex m_folderSplitter->setSizes({300, 380, 600}); mailLayout->addWidget(m_folderSplitter); } 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); 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 &) { 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) { switchToPage(static_cast(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 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(); // Clear selection and show placeholder m_currentMailId = -1; m_mailListView->treeView()->clearSelection(); m_viewerStack->setCurrentIndex(0); // placeholder } } void MainMainWindow::onEmailSelected(int mailId) { m_currentMailId = mailId; if (mailId >= 0) { std::optional 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); m_viewerStack->setCurrentIndex(0); // placeholder } } void MainMainWindow::onComposeRequested() { m_embeddedComposeView->initializeComposition(); m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::onReaderReplyRequested(int mailId) { std::optional item = MailItemDao::findById(mailId); if (item.has_value()) { m_embeddedComposeView->initializeComposition(); m_embeddedComposeView->setTo(item->sender()); m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject())); // Insert quoted original message BELOW the signature QString quoted = QString("
%2
").arg(item->sender(), item->bodyHtml()); // Get current body (with signature) and append quoted text QString currentBody = m_embeddedComposeView->getBody(); if (!currentBody.isEmpty()) { m_embeddedComposeView->setBody(currentBody + "

" + quoted); } else { m_embeddedComposeView->setBody(quoted); } } m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::onReaderForwardRequested(int mailId) { std::optional item = MailItemDao::findById(mailId); if (item.has_value()) { m_embeddedComposeView->initializeComposition(); m_embeddedComposeView->setSubject(QString("Fwd: %1").arg(item->subject())); // Insert forwarded message BELOW the signature QString quoted = QString("
%2
").arg(item->sender(), item->bodyHtml()); QString currentBody = m_embeddedComposeView->getBody(); if (!currentBody.isEmpty()) { m_embeddedComposeView->setBody(currentBody + "

" + quoted); } else { m_embeddedComposeView->setBody(quoted); } } m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::onReaderDeleteRequested(int mailId) { MailItemDao::remove(mailId); if (m_currentMailId == mailId) { m_currentMailId = -1; m_viewerStack->setCurrentIndex(0); // placeholder } m_emailModel->refresh(); statusBar()->showMessage(tr("Mensaje eliminado"), 3000); } // MailListView action handlers (from compact delegate) void MainMainWindow::onFlagRequested(int mailId) { auto item = MailItemDao::findById(mailId); if (item.has_value()) { item->setFlagged(!item->isFlagged()); MailItemDao::update(*item); m_emailModel->refresh(); if (m_currentMailId == mailId) { m_emailViewer->setMailItem(&*item); } statusBar()->showMessage(tr(item->isFlagged() ? "Marcado para seguimiento" : "Seguimiento quitado"), 2000); } } void MainMainWindow::onDeleteRequested(int mailId) { MailItemDao::remove(mailId); if (m_currentMailId == mailId) { m_currentMailId = -1; m_viewerStack->setCurrentIndex(0); // placeholder } m_emailModel->refresh(); statusBar()->showMessage(tr("Mensaje eliminado"), 3000); } void MainMainWindow::onCategoryRequested(int mailId) { // Show category context menu at cursor position // For now, just show status - could open a dialog statusBar()->showMessage(tr("Categoría para mensaje %1").arg(mailId), 2000); } void MainMainWindow::onMoreRequested(int mailId, const QPoint &globalPos) { // Show context menu with more actions QMenu menu; menu.addAction("Responder", [this, mailId]() { onReaderReplyRequested(mailId); }); menu.addAction("Reenviar", [this, mailId]() { onReaderForwardRequested(mailId); }); menu.addAction("Mover a...", [this, mailId]() { /* TODO: move dialog */ }); menu.addAction("Marcar como no leído", [this, mailId]() { auto item = MailItemDao::findById(mailId); if (item.has_value()) { item->setRead(false); MailItemDao::update(*item); m_emailModel->refresh(); } }); menu.addSeparator(); menu.addAction("Eliminar", [this, mailId]() { onDeleteRequested(mailId); }); menu.exec(globalPos); } void MainMainWindow::onNewMessage() { // Show compose view in the viewer stack m_embeddedComposeView->initializeComposition(); m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::openMailInIndependentWindow(int mailId) { std::optional 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); } // Category slots void MainMainWindow::updateCategoryTreeForAccount(qint64 accountId) { if (m_categoryTree) { m_categoryTree->setAccountId(accountId); } } void MainMainWindow::onCategorySelected(const Category& cat) { // Filter mail list by category - could be implemented statusBar()->showMessage(tr("Categoría seleccionada: %1").arg(cat.name), 2000); } void MainMainWindow::onCategoryDoubleClicked(const Category& cat) { // Edit category onCategoryEdited(cat); } void MainMainWindow::onAssignCategoryRequested(qint64 mailId, qint64 categoryId) { assignCategoryToSelectedMails(categoryId); } void MainMainWindow::assignCategoryToSelectedMails(qint64 categoryId) { // Get selected mail IDs from mail list view // This requires MailListView to expose selected mail IDs // For now, use current mail if no multi-selection if (m_currentMailId >= 0) { if (CategoryDao::assignToMail(m_currentMailId, categoryId)) { statusBar()->showMessage(tr("Categoría asignada"), 2000); // Refresh category tree checkboxes if (m_categoryTree) { m_categoryTree->onMailSelected(m_currentMailId); } } } } void MainMainWindow::onCategoryCreated(const Category& cat) { statusBar()->showMessage(tr("Categoría creada: %1").arg(cat.name), 3000); } void MainMainWindow::onCategoryEdited(const Category& cat) { statusBar()->showMessage(tr("Categoría actualizada: %1").arg(cat.name), 3000); } void MainMainWindow::onCategoryDeleted(qint64 categoryId) { statusBar()->showMessage(tr("Categoría eliminada"), 3000); } // Rules slots void MainMainWindow::onRulesChanged() { statusBar()->showMessage(tr("Reglas actualizadas"), 2000); } void MainMainWindow::onRunRulesOnFolder() { if (m_currentFolderId >= 0) { QMessageBox::information(this, tr("Ejecutar reglas"), tr("Ejecutando reglas en carpeta actual...")); int processed = RulesEngine::runRulesOnFolder(m_currentFolderId, -1); statusBar()->showMessage(tr("Reglas ejecutadas en %1 correos").arg(processed), 5000); m_emailModel->refresh(); } } void MainMainWindow::onRunRulesOnSelected() { // Get selected mail IDs QMessageBox::information(this, tr("Ejecutar reglas"), tr("Ejecutando reglas en correos seleccionados...")); // TODO: Get selected mail IDs from MailListView statusBar()->showMessage(tr("Reglas ejecutadas"), 3000); } void MainMainWindow::showRulesManager() { RulesManagerDialog dlg(-1, this); // Global rules connect(&dlg, &RulesManagerDialog::rulesChanged, this, &MainMainWindow::onRulesChanged); dlg.exec(); } void MainMainWindow::showTemplatesManager() { qint64 accountId = -1; // Could get current account from folder selection TemplatesManagerDialog dlg(accountId, this); connect(&dlg, &TemplatesManagerDialog::templateSelected, [this](const Template& tmpl) { // If compose view is active, apply template if (m_viewerStack->currentWidget() == m_embeddedComposeView) { m_embeddedComposeView->applyTemplate(tmpl); } }); connect(&dlg, &TemplatesManagerDialog::templatesChanged, this, [this]() { statusBar()->showMessage(tr("Plantillas actualizadas"), 2000); }); dlg.exec(); } void MainMainWindow::createTitleBar() { // Enable frameless window Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint; setWindowFlags(flags); #ifdef Q_OS_WIN HWND hwnd = (HWND)this->winId(); DWORD style = ::GetWindowLong(hwnd, GWL_STYLE); ::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION); #endif // Remove status bar (item 4) setStatusBar(nullptr); // Apply rounded corners and border to main window (items 1, 2) // Use mask for rounded corners instead of WA_TranslucentBackground to avoid black corners setStyleSheet( "MainMainWindow {" " background-color: #f5f5f7;" " border: 1px solid #d1d1d6;" " border-radius: 8px;" "}" "QWidget#TitleBar {" " background-color: #ffffff;" " border-bottom: 1px solid #d1d1d6;" " border-top-left-radius: 8px;" " border-top-right-radius: 8px;" "}" "QToolButton {" " border: none;" " background: transparent;" " border-radius: 4px;" " padding: 4px;" "}" "QToolButton:hover {" " background-color: #e8e8ed;" "}" "QToolButton:pressed {" " background-color: #d1d1d6;" "}" "QLabel {" " color: #1d1d1f;" " font-weight: 600;" " font-size: 13px;" "}" ); // Create title bar widget m_titleBar = new QWidget(this); m_titleBar->setObjectName("TitleBar"); m_titleBar->setFixedHeight(36); QHBoxLayout *layout = new QHBoxLayout(m_titleBar); layout->setContentsMargins(8, 0, 8, 0); layout->setSpacing(4); // App icon QLabel *iconLabel = new QLabel(); iconLabel->setPixmap(QIcon(":/icons/app.svg").pixmap(18, 18)); layout->addWidget(iconLabel); // Window title QLabel *titleLabel = new QLabel("Wino Mail"); layout->addWidget(titleLabel); layout->addStretch(); // Custom action buttons (sync, settings, etc.) auto addActionButton = [&](const QString &iconName, const QString &tooltip, std::function slot) { QToolButton *btn = new QToolButton(); btn->setIcon(QIcon(QString(":/icons/%1.svg").arg(iconName))); btn->setToolTip(tooltip); btn->setFixedSize(28, 28); btn->setIconSize(QSize(16, 16)); connect(btn, &QToolButton::clicked, this, slot); layout->addWidget(btn); return btn; }; addActionButton("sync", "Sincronizar", [this]() { onSyncRequested(); }); addActionButton("settings", "Ajustes", [this]() { onSettingsRequested(); }); addActionButton("filter", "Reglas", [this]() { showRulesManager(); }); addActionButton("play", "Ejecutar reglas en carpeta", [this]() { onRunRulesOnFolder(); }); addActionButton("file-text", "Plantillas", [this]() { showTemplatesManager(); }); layout->addSpacing(8); // Window control buttons (minimize, maximize, close) QToolButton *minBtn = new QToolButton(); minBtn->setIcon(QIcon(":/icons/minimize.svg")); minBtn->setToolTip("Minimizar"); minBtn->setFixedSize(28, 28); minBtn->setIconSize(QSize(16, 16)); connect(minBtn, &QToolButton::clicked, this, &QWidget::showMinimized); layout->addWidget(minBtn); m_maximizeButton = new QToolButton(); m_maximizeButton->setIcon(QIcon(isMaximized() ? ":/icons/restore.svg" : ":/icons/maximize.svg")); m_maximizeButton->setToolTip(isMaximized() ? "Restaurar" : "Maximizar"); m_maximizeButton->setFixedSize(28, 28); m_maximizeButton->setIconSize(QSize(16, 16)); connect(m_maximizeButton, &QToolButton::clicked, this, [this]() { if (isMaximized()) { showNormal(); } else { showMaximized(); } }); layout->addWidget(m_maximizeButton); m_closeButton = new QToolButton(); m_closeButton->setIcon(QIcon(":/icons/close.svg")); m_closeButton->setToolTip("Cerrar"); m_closeButton->setFixedSize(28, 28); m_closeButton->setIconSize(QSize(16, 16)); m_closeButton->setStyleSheet( "QToolButton {" " border: none;" " background: transparent;" " border-radius: 4px;" " padding: 4px;" "}" "QToolButton:hover {" " background-color: #ff3b30;" " color: white;" "}" "QToolButton:pressed {" " background-color: #e60000;" "}" ); connect(m_closeButton, &QToolButton::clicked, this, &QWidget::close); layout->addWidget(m_closeButton); // Set title bar as menu widget (appears in native title bar area on some platforms) setMenuWidget(m_titleBar); } void MainMainWindow::onSyncRequested() { if (m_currentFolderId >= 0) { const auto folder = FolderDao::findById(m_currentFolderId); if (folder) { m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId)); statusBar()->showMessage(tr("Sincronizando…"), 3000); } } } void MainMainWindow::onSettingsRequested() { switchToPage(PageSettings); } void MainMainWindow::updateMaximizeButtonIcon() { if (m_maximizeButton) { m_maximizeButton->setIcon(QIcon(isMaximized() ? ":/icons/restore.svg" : ":/icons/maximize.svg")); m_maximizeButton->setToolTip(isMaximized() ? "Restaurar" : "Maximizar"); } } // Frameless window mouse handling void MainMainWindow::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { QPoint pos = event->position().toPoint(); // Check if clicking on resize edges (8px margin) int margin = 8; QRect windowRect = this->rect(); bool onLeft = pos.x() <= margin; bool onRight = pos.x() >= windowRect.width() - margin; bool onTop = pos.y() <= margin; bool onBottom = pos.y() >= windowRect.height() - margin; m_resizeEdges = Qt::Edges(); if (onTop) m_resizeEdges |= Qt::TopEdge; if (onBottom) m_resizeEdges |= Qt::BottomEdge; if (onLeft) m_resizeEdges |= Qt::LeftEdge; if (onRight) m_resizeEdges |= Qt::RightEdge; if (m_resizeEdges) { m_resizeStartPos = event->globalPosition().toPoint(); m_resizeStartGeom = frameGeometry(); m_resizing = true; event->accept(); return; } // Only allow dragging from title bar area (top 36px) if (pos.y() <= 36) { m_dragPos = event->globalPosition().toPoint() - frameGeometry().topLeft(); m_dragging = true; event->accept(); } } QMainWindow::mousePressEvent(event); } void MainMainWindow::mouseMoveEvent(QMouseEvent *event) { // Update cursor when not dragging/resizing if (!m_dragging && !m_resizing) { QPoint pos = event->position().toPoint(); int margin = 8; QRect windowRect = this->rect(); bool onLeft = pos.x() <= margin; bool onRight = pos.x() >= windowRect.width() - margin; bool onTop = pos.y() <= margin; bool onBottom = pos.y() >= windowRect.height() - margin; Qt::Edges edges = Qt::Edges(); if (onTop) edges |= Qt::TopEdge; if (onBottom) edges |= Qt::BottomEdge; if (onLeft) edges |= Qt::LeftEdge; if (onRight) edges |= Qt::RightEdge; if (edges & Qt::TopEdge && edges & Qt::LeftEdge) setCursor(Qt::SizeFDiagCursor); else if (edges & Qt::TopEdge && edges & Qt::RightEdge) setCursor(Qt::SizeBDiagCursor); else if (edges & Qt::BottomEdge && edges & Qt::LeftEdge) setCursor(Qt::SizeBDiagCursor); else if (edges & Qt::BottomEdge && edges & Qt::RightEdge) setCursor(Qt::SizeFDiagCursor); else if (edges & Qt::LeftEdge || edges & Qt::RightEdge) setCursor(Qt::SizeHorCursor); else if (edges & Qt::TopEdge || edges & Qt::BottomEdge) setCursor(Qt::SizeVerCursor); else unsetCursor(); } if (m_resizing && (event->buttons() & Qt::LeftButton)) { QPoint delta = event->globalPosition().toPoint() - m_resizeStartPos; QRect newGeom = m_resizeStartGeom; if (m_resizeEdges & Qt::LeftEdge) { int newLeft = m_resizeStartGeom.left() + delta.x(); int minWidth = minimumWidth(); if (m_resizeStartGeom.right() - newLeft >= minWidth) { newGeom.setLeft(newLeft); } } if (m_resizeEdges & Qt::RightEdge) { int newWidth = m_resizeStartGeom.width() + delta.x(); if (newWidth >= minimumWidth()) { newGeom.setWidth(newWidth); } } if (m_resizeEdges & Qt::TopEdge) { int newTop = m_resizeStartGeom.top() + delta.y(); int minHeight = minimumHeight(); if (m_resizeStartGeom.bottom() - newTop >= minHeight) { newGeom.setTop(newTop); } } if (m_resizeEdges & Qt::BottomEdge) { int newHeight = m_resizeStartGeom.height() + delta.y(); if (newHeight >= minimumHeight()) { newGeom.setHeight(newHeight); } } setGeometry(newGeom); event->accept(); return; } if (m_dragging && (event->buttons() & Qt::LeftButton)) { move(event->globalPosition().toPoint() - m_dragPos); event->accept(); } QMainWindow::mouseMoveEvent(event); } void MainMainWindow::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_dragging = false; m_resizing = false; m_resizeEdges = Qt::Edges(); unsetCursor(); event->accept(); } QMainWindow::mouseReleaseEvent(event); } bool MainMainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) { #ifdef Q_OS_WIN // Handle Windows non-client area hits for resize MSG *msg = static_cast(message); if (msg->message == WM_NCHITTEST) { // Let Windows handle resize borders, but we handle caption // This allows native resize from edges *result = 0; return false; // Let Windows handle it } #endif return QMainWindow::nativeEvent(eventType, message, result); } void MainMainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::WindowStateChange) { updateMaximizeButtonIcon(); m_lastWindowState = windowState(); } QMainWindow::changeEvent(event); } void MainMainWindow::onProgressChanged(int percent) { Q_UNUSED(percent); // No progress bar in status bar anymore } bool MainMainWindow::eventFilter(QObject *watched, QEvent *event) { // Handle resize from central widget edges if (watched == centralWidget()) { if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *me = static_cast(event); if (me->button() == Qt::LeftButton) { // Forward to main window's mousePressEvent mousePressEvent(me); return true; } } else if (event->type() == QEvent::MouseMove) { QMouseEvent *me = static_cast(event); mouseMoveEvent(me); return true; } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent *me = static_cast(event); if (me->button() == Qt::LeftButton) { mouseReleaseEvent(me); return true; } } } return QMainWindow::eventFilter(watched, event); } void MainMainWindow::onStatusMessage(const QString &msg) { Q_UNUSED(msg); // No status bar anymore } 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) { 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(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); }