2026-06-17 22:44:27 +02:00
|
|
|
#include "mainmainwindow.h"
|
|
|
|
|
#include "core/models/account.h"
|
|
|
|
|
#include "core/mailitem.h"
|
|
|
|
|
#include "db/dao/mailitemdao.h"
|
2026-06-25 18:10:49 +02:00
|
|
|
#include "db/dao/folderdao.h"
|
2026-08-23 14:49:58 +02:00
|
|
|
#include "db/dao/categorydao.h"
|
|
|
|
|
#include "db/dao/ruledao.h"
|
|
|
|
|
#include "services/rulesengine.h"
|
|
|
|
|
#include "ui/rulesmanagerdialog.h"
|
|
|
|
|
#include "ui/accountsetupdialog.h"
|
2026-08-24 23:25:13 +02:00
|
|
|
#include "ui/delegates/FolderTreeDelegate.h"
|
2026-06-25 18:10:49 +02:00
|
|
|
#include <optional>
|
|
|
|
|
#include <QMessageBox>
|
2026-08-23 14:49:58 +02:00
|
|
|
#include <QFileDialog>
|
2026-06-17 22:44:27 +02:00
|
|
|
#include <QVBoxLayout>
|
|
|
|
|
#include <QHBoxLayout>
|
|
|
|
|
#include <QFrame>
|
|
|
|
|
#include <QDateTime>
|
|
|
|
|
#include <QLabel>
|
2026-08-23 14:49:58 +02:00
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
#endif
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
MainMainWindow::MainMainWindow(QWidget *parent)
|
2026-06-23 01:34:13 +02:00
|
|
|
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
|
2026-06-17 22:44:27 +02:00
|
|
|
{
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow] Constructor start";
|
2026-08-23 16:59:17 +02:00
|
|
|
// Enable mouse tracking for resize cursor
|
|
|
|
|
setMouseTracking(true);
|
2026-06-17 22:44:27 +02:00
|
|
|
setupUI();
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow] setupUI done";
|
2026-06-17 22:44:27 +02:00
|
|
|
connectModels();
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow] connectModels done";
|
2026-06-17 22:44:27 +02:00
|
|
|
setWindowTitle("Wino Mail DTK");
|
|
|
|
|
resize(1280, 820);
|
2026-08-23 16:59:17 +02:00
|
|
|
// Note: status bar is removed in createTitleBar(), no progress bar in status bar
|
2026-07-05 10:45:44 +02:00
|
|
|
// Connect mail service progress/status signals
|
|
|
|
|
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
|
|
|
|
|
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
|
2026-08-17 15:34:49 +02:00
|
|
|
connect(m_mailService, &MailService::mailSent, this, [this](const QString &) {
|
2026-08-23 16:59:17 +02:00
|
|
|
// statusBar()->showMessage(tr("Message sent"), 5000); // no status bar
|
2026-08-17 15:34:49 +02:00
|
|
|
});
|
|
|
|
|
connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) {
|
2026-08-23 16:59:17 +02:00
|
|
|
// statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000); // no status bar
|
2026-08-17 15:34:49 +02:00
|
|
|
});
|
2026-07-05 10:45:44 +02:00
|
|
|
// Provide account service to settings view
|
|
|
|
|
m_settingsView->setAccountService(m_accountService);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow] Constructor complete";
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::setupUI()
|
|
|
|
|
{
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Start";
|
2026-06-17 22:44:27 +02:00
|
|
|
// === 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; }"
|
|
|
|
|
);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
createTitleBar();
|
|
|
|
|
qDebug() << "[MainMainWindow::setupUI] Title bar created";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
// === Central widget ===
|
|
|
|
|
QWidget *central = new QWidget();
|
2026-08-23 16:59:17 +02:00
|
|
|
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);
|
2026-06-17 22:44:27 +02:00
|
|
|
QHBoxLayout *centralLayout = new QHBoxLayout(central);
|
|
|
|
|
centralLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
centralLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
// === Sidebar ===
|
|
|
|
|
setupSidebar();
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Sidebar created";
|
2026-06-17 22:44:27 +02:00
|
|
|
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;");
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Stack created";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
// Page 0: Mail (folder tree + mail list + reader)
|
|
|
|
|
setupMailPage();
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Mail page created";
|
2026-06-17 22:44:27 +02:00
|
|
|
m_stack->addWidget(m_mailPage);
|
|
|
|
|
|
|
|
|
|
// Page 1: Compose
|
|
|
|
|
m_composeView = new ComposeView();
|
2026-08-17 15:34:49 +02:00
|
|
|
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) {
|
2026-06-17 22:44:27 +02:00
|
|
|
if (scheduleTime.isValid()) {
|
2026-08-17 15:34:49 +02:00
|
|
|
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
|
|
|
|
|
return;
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
2026-08-17 15:34:49 +02:00
|
|
|
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);
|
2026-06-17 22:44:27 +02:00
|
|
|
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");
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-17 22:44:27 +02:00
|
|
|
// Assign central widget and ensure it's visible and sized
|
|
|
|
|
detachedWin->setCentralWidget(composeView);
|
|
|
|
|
composeView->setMinimumSize(800, 600);
|
|
|
|
|
composeView->update();
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-17 22:44:27 +02:00
|
|
|
detachedWin->resize(800, 600);
|
|
|
|
|
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
detachedWin->show();
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-17 22:44:27 +02:00
|
|
|
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]() {
|
2026-06-23 01:34:13 +02:00
|
|
|
AccountSetupDialog dlg(m_accountService, this);
|
|
|
|
|
dlg.exec();
|
2026-06-17 22:44:27 +02:00
|
|
|
});
|
2026-07-05 10:45:44 +02:00
|
|
|
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
|
|
|
|
|
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
|
2026-06-17 22:44:27 +02:00
|
|
|
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);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Settings page created";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Central widget set";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
// Show mail page by default
|
|
|
|
|
switchToPage(PageMail);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::setupUI] Done";
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::setupSidebar()
|
|
|
|
|
{
|
2026-06-17 22:44:27 +02:00
|
|
|
m_sidebar = new QListWidget();
|
2026-07-05 15:32:12 +02:00
|
|
|
m_sidebar->setFixedWidth(100);
|
2026-06-17 22:44:27 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::setupMailPage()
|
2026-08-17 15:34:49 +02:00
|
|
|
{ m_mailPage = new QWidget();
|
2026-06-17 22:44:27 +02:00
|
|
|
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
|
|
|
|
|
mailLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
mailLayout->setSpacing(0);
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
// Main splitter with 3 panes: folders | categories | mail list | viewer
|
2026-06-17 22:44:27 +02:00
|
|
|
m_folderSplitter = new QSplitter(Qt::Horizontal);
|
|
|
|
|
m_folderSplitter->setHandleWidth(1);
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
// Left pane: Folder tree + Category tree (vertical splitter)
|
|
|
|
|
QSplitter* leftSplitter = new QSplitter(Qt::Vertical);
|
|
|
|
|
leftSplitter->setHandleWidth(1);
|
|
|
|
|
leftSplitter->setChildrenCollapsible(false);
|
|
|
|
|
|
2026-06-17 22:44:27 +02:00
|
|
|
// 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);
|
2026-08-24 23:25:13 +02:00
|
|
|
m_folderTree->setItemDelegate(new FolderTreeDelegate(this)); // Custom delegate with unread count badges
|
2026-08-23 14:49:58 +02:00
|
|
|
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);
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2026-06-18 18:58:27 +02:00
|
|
|
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
|
|
|
|
|
openMailInIndependentWindow(mailId);
|
|
|
|
|
});
|
2026-08-24 23:25:13 +02:00
|
|
|
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);
|
2026-06-17 22:44:27 +02:00
|
|
|
m_folderSplitter->addWidget(m_mailListView);
|
|
|
|
|
|
2026-08-17 15:34:49 +02:00
|
|
|
// 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
|
2026-06-17 22:44:27 +02:00
|
|
|
m_emailViewer = new ReaderView();
|
|
|
|
|
m_emailViewer->setMinimumWidth(350);
|
|
|
|
|
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
|
2026-08-23 14:49:58 +02:00
|
|
|
connect(m_emailViewer, &ReaderView::forwardRequested, this, &MainMainWindow::onReaderForwardRequested);
|
|
|
|
|
connect(m_emailViewer, &ReaderView::deleteRequested, this, &MainMainWindow::onReaderDeleteRequested);
|
2026-06-18 18:58:27 +02:00
|
|
|
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
|
2026-06-23 01:34:13 +02:00
|
|
|
if (m_currentMailId >= 0) {
|
|
|
|
|
openMailInIndependentWindow(m_currentMailId);
|
2026-06-18 18:58:27 +02:00
|
|
|
}
|
|
|
|
|
});
|
2026-06-17 22:44:27 +02:00
|
|
|
|
2026-08-17 15:34:49 +02:00
|
|
|
// 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);
|
2026-08-23 14:49:58 +02:00
|
|
|
connect(m_embeddedComposeView, &ComposeView::statusMessageRequested, this, [this](const QString& msg) {
|
|
|
|
|
statusBar()->showMessage(msg, 3000);
|
|
|
|
|
});
|
2026-08-17 15:34:49 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
// Default sizes: left pane (folders+categories) 300, list 380, viewer flex
|
|
|
|
|
m_folderSplitter->setSizes({300, 380, 600});
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
mailLayout->addWidget(m_folderSplitter);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::connectModels()
|
|
|
|
|
{
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::connectModels] Start";
|
2026-06-17 22:44:27 +02:00
|
|
|
m_accountService = new AccountService(this);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::connectModels] AccountService created";
|
2026-07-05 10:45:44 +02:00
|
|
|
m_mailService = new MailService(m_accountService, this);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::connectModels] MailService created";
|
2026-07-05 10:45:44 +02:00
|
|
|
m_composeView->setAccountService(m_accountService);
|
2026-08-17 15:34:49 +02:00
|
|
|
m_embeddedComposeView->setAccountService(m_accountService);
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
m_folderModel = new FolderListModel(m_accountService, this);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::connectModels] FolderListModel created";
|
2026-06-17 22:44:27 +02:00
|
|
|
m_emailModel = new EmailListModel(this);
|
2026-08-17 15:34:49 +02:00
|
|
|
qDebug() << "[MainMainWindow::connectModels] EmailListModel created";
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
m_folderTree->setModel(m_folderModel);
|
|
|
|
|
m_mailListView->setModel(m_emailModel);
|
|
|
|
|
|
|
|
|
|
m_folderTree->expandAll();
|
|
|
|
|
|
|
|
|
|
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
|
2026-08-17 15:34:49 +02:00
|
|
|
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";
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::onNavChanged(int index)
|
|
|
|
|
{
|
2026-06-17 22:44:27 +02:00
|
|
|
switchToPage(static_cast<Page>(index));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::switchToPage(int pageIndex)
|
|
|
|
|
{
|
2026-06-17 22:44:27 +02:00
|
|
|
m_stack->setCurrentIndex(pageIndex);
|
|
|
|
|
m_sidebar->blockSignals(true);
|
|
|
|
|
m_sidebar->setCurrentRow(pageIndex);
|
|
|
|
|
m_sidebar->blockSignals(false);
|
|
|
|
|
|
|
|
|
|
// Show/hide toolbar actions per page
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::onFolderSelected(const QModelIndex &index)
|
2026-08-17 15:34:49 +02:00
|
|
|
{ if (!index.isValid()) return;
|
2026-06-17 22:44:27 +02:00
|
|
|
|
|
|
|
|
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
|
|
|
|
|
if (itemType == FolderTreeItem::FolderNode) {
|
|
|
|
|
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
|
|
|
|
|
m_emailModel->setFolderId(m_currentFolderId);
|
2026-06-25 18:10:49 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
m_emailModel->refresh();
|
2026-08-17 15:34:49 +02:00
|
|
|
// Clear selection and show placeholder
|
|
|
|
|
m_currentMailId = -1;
|
2026-08-24 01:04:00 +02:00
|
|
|
m_mailListView->treeView()->clearSelection();
|
2026-08-17 15:34:49 +02:00
|
|
|
m_viewerStack->setCurrentIndex(0); // placeholder
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::onEmailSelected(int mailId)
|
2026-08-17 15:34:49 +02:00
|
|
|
{ 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 {
|
2026-06-17 22:44:27 +02:00
|
|
|
m_emailViewer->setMailItem(nullptr);
|
2026-08-17 15:34:49 +02:00
|
|
|
m_viewerStack->setCurrentIndex(0); // placeholder
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::onComposeRequested()
|
2026-08-17 15:34:49 +02:00
|
|
|
{ m_embeddedComposeView->initializeComposition();
|
|
|
|
|
m_viewerStack->setCurrentIndex(2); // compose
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
void MainMainWindow::onReaderReplyRequested(int mailId)
|
|
|
|
|
{
|
|
|
|
|
std::optional<MailItem> item = MailItemDao::findById(mailId);
|
|
|
|
|
if (item.has_value()) {
|
2026-08-17 15:34:49 +02:00
|
|
|
m_embeddedComposeView->initializeComposition();
|
|
|
|
|
m_embeddedComposeView->setTo(item->sender());
|
|
|
|
|
m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject()));
|
2026-08-23 14:49:58 +02:00
|
|
|
// Insert quoted original message BELOW the signature
|
|
|
|
|
QString quoted = QString("<blockquote cite=\"mailto:%1\">%2</blockquote>").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 + "<br><br>" + quoted);
|
|
|
|
|
} else {
|
|
|
|
|
m_embeddedComposeView->setBody(quoted);
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
2026-08-17 15:34:49 +02:00
|
|
|
m_viewerStack->setCurrentIndex(2); // compose
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
void MainMainWindow::onReaderForwardRequested(int mailId)
|
|
|
|
|
{
|
|
|
|
|
std::optional<MailItem> 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("<blockquote cite=\"mailto:%1\">%2</blockquote>").arg(item->sender(), item->bodyHtml());
|
|
|
|
|
QString currentBody = m_embeddedComposeView->getBody();
|
|
|
|
|
if (!currentBody.isEmpty()) {
|
|
|
|
|
m_embeddedComposeView->setBody(currentBody + "<br><br>" + 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 23:25:13 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::onNewMessage()
|
2026-08-17 15:34:49 +02:00
|
|
|
{ // Show compose view in the viewer stack
|
|
|
|
|
m_embeddedComposeView->initializeComposition();
|
|
|
|
|
m_viewerStack->setCurrentIndex(2); // compose
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
void MainMainWindow::openMailInIndependentWindow(int mailId)
|
|
|
|
|
{
|
2026-06-18 18:58:27 +02:00
|
|
|
std::optional<MailItem> item = MailItemDao::findById(mailId);
|
|
|
|
|
if (!item.has_value()) return;
|
|
|
|
|
|
|
|
|
|
QMainWindow *detachedWin = new QMainWindow();
|
|
|
|
|
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-18 18:58:27 +02:00
|
|
|
ReaderView *detachedReader = new ReaderView();
|
|
|
|
|
detachedReader->setMailItem(&item.value());
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-18 18:58:27 +02:00
|
|
|
detachedWin->setCentralWidget(detachedReader);
|
|
|
|
|
detachedWin->resize(800, 600);
|
|
|
|
|
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
detachedWin->show();
|
2026-07-05 10:45:44 +02:00
|
|
|
|
2026-06-18 18:58:27 +02:00
|
|
|
statusBar()->showMessage("Opened mail in independent window", 3000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
// Category slots
|
|
|
|
|
void MainMainWindow::updateCategoryTreeForAccount(qint64 accountId)
|
2026-07-05 10:45:44 +02:00
|
|
|
{
|
2026-08-23 14:49:58 +02:00
|
|
|
if (m_categoryTree) {
|
|
|
|
|
m_categoryTree->setAccountId(accountId);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
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);
|
2026-08-17 15:34:49 +02:00
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
2026-08-23 14:49:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
});
|
2026-08-23 14:49:58 +02:00
|
|
|
connect(&dlg, &TemplatesManagerDialog::templatesChanged, this, [this]() {
|
|
|
|
|
statusBar()->showMessage(tr("Plantillas actualizadas"), 2000);
|
|
|
|
|
});
|
|
|
|
|
dlg.exec();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainMainWindow::createTitleBar()
|
|
|
|
|
{
|
|
|
|
|
// Enable frameless window
|
2026-08-23 16:59:17 +02:00
|
|
|
Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;
|
|
|
|
|
setWindowFlags(flags);
|
2026-08-23 14:49:58 +02:00
|
|
|
|
|
|
|
|
#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)
|
2026-08-23 16:59:17 +02:00
|
|
|
setStatusBar(nullptr);
|
2026-08-23 14:49:58 +02:00
|
|
|
|
|
|
|
|
// Apply rounded corners and border to main window (items 1, 2)
|
2026-08-23 16:59:17 +02:00
|
|
|
// Use mask for rounded corners instead of WA_TranslucentBackground to avoid black corners
|
2026-08-23 14:49:58 +02:00
|
|
|
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<void()> 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();
|
2026-06-23 01:34:13 +02:00
|
|
|
} else {
|
2026-08-23 14:49:58 +02:00
|
|
|
showMaximized();
|
2026-06-23 01:34:13 +02:00
|
|
|
}
|
|
|
|
|
});
|
2026-08-23 14:49:58 +02:00
|
|
|
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);
|
2026-08-17 15:34:49 +02:00
|
|
|
}
|
2026-08-23 14:49:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<MSG *>(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);
|
2026-07-05 10:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainMainWindow::onProgressChanged(int percent)
|
|
|
|
|
{
|
2026-08-23 16:59:17 +02:00
|
|
|
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<QMouseEvent*>(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<QMouseEvent*>(event);
|
|
|
|
|
mouseMoveEvent(me);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (event->type() == QEvent::MouseButtonRelease) {
|
|
|
|
|
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
|
|
|
|
if (me->button() == Qt::LeftButton) {
|
|
|
|
|
mouseReleaseEvent(me);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-05 10:45:44 +02:00
|
|
|
}
|
2026-08-23 16:59:17 +02:00
|
|
|
return QMainWindow::eventFilter(watched, event);
|
2026-07-05 10:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainMainWindow::onStatusMessage(const QString &msg)
|
|
|
|
|
{
|
2026-08-23 16:59:17 +02:00
|
|
|
Q_UNUSED(msg);
|
|
|
|
|
// No status bar anymore
|
2026-07-05 10:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2026-08-17 15:34:49 +02:00
|
|
|
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);
|
|
|
|
|
}
|