Merge "Add shortcut for focusing mailbox tree and message list"
[trojita.git] / src / Gui / Window.cpp
blobf4d7335470ce0cf03fc7121a4d4442a1a363c429
1 /* Copyright (C) 2006 - 2015 Jan Kundrát <jkt@flaska.net>
2 Copyright (C) 2013 - 2015 Pali Rohár <pali.rohar@gmail.com>
4 This file is part of the Trojita Qt IMAP e-mail client,
5 http://trojita.flaska.net/
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License as
9 published by the Free Software Foundation; either version 2 of
10 the License or (at your option) version 3 or any later version
11 accepted by the membership of KDE e.V. (or its successor approved
12 by the membership of KDE e.V.), which shall act as a proxy
13 defined in Section 14 of version 3 of the license.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
20 You should have received a copy of the GNU General Public License
21 along with this program. If not, see <http://www.gnu.org/licenses/>.
24 #include <QAuthenticator>
25 #include <QDesktopServices>
26 #include <QDesktopWidget>
27 #include <QDir>
28 #include <QDockWidget>
29 #include <QFileDialog>
30 #include <QHeaderView>
31 #include <QItemSelectionModel>
32 #include <QKeyEvent>
33 #include <QMenuBar>
34 #include <QMessageBox>
35 #include <QProgressBar>
36 #include <QScrollBar>
37 #include <QSplitter>
38 #include <QSslError>
39 #include <QSslKey>
40 #include <QStackedWidget>
41 #include <QStatusBar>
42 #include <QTextDocument>
43 #include <QToolBar>
44 #include <QToolButton>
45 #include <QToolTip>
46 #include <QUrl>
47 #include <QWheelEvent>
49 #include "configure.cmake.h"
50 #include "Common/Application.h"
51 #include "Common/Paths.h"
52 #include "Common/PortNumbers.h"
53 #include "Common/SettingsNames.h"
54 #include "Composer/Mailto.h"
55 #include "Composer/SenderIdentitiesModel.h"
56 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
57 # ifdef TROJITA_HAVE_GPGMEPP
58 # include "Cryptography/GpgMe++.h"
59 # endif
60 #endif
61 #include "Imap/Model/ImapAccess.h"
62 #include "Imap/Model/MailboxTree.h"
63 #include "Imap/Model/Model.h"
64 #include "Imap/Model/ModelWatcher.h"
65 #include "Imap/Model/MsgListModel.h"
66 #include "Imap/Model/NetworkWatcher.h"
67 #include "Imap/Model/PrettyMailboxModel.h"
68 #include "Imap/Model/PrettyMsgListModel.h"
69 #include "Imap/Model/SpecialFlagNames.h"
70 #include "Imap/Model/ThreadingMsgListModel.h"
71 #include "Imap/Model/Utils.h"
72 #include "Imap/Tasks/ImapTask.h"
73 #include "Imap/Network/FileDownloadManager.h"
74 #include "MSA/ImapSubmit.h"
75 #include "MSA/Sendmail.h"
76 #include "MSA/SMTP.h"
77 #include "Plugins/AddressbookPlugin.h"
78 #include "Plugins/PasswordPlugin.h"
79 #include "Plugins/PluginManager.h"
80 #include "CompleteMessageWidget.h"
81 #include "ComposeWidget.h"
82 #include "MailBoxTreeView.h"
83 #include "MessageListWidget.h"
84 #include "MessageView.h"
85 #include "MessageSourceWidget.h"
86 #include "Gui/MessageHeadersWidget.h"
87 #include "MsgListView.h"
88 #include "OnePanelAtTimeWidget.h"
89 #include "PasswordDialog.h"
90 #include "ProtocolLoggerWidget.h"
91 #include "SettingsDialog.h"
92 #include "SimplePartWidget.h"
93 #include "Streams/SocketFactory.h"
94 #include "TaskProgressIndicator.h"
95 #include "Util.h"
96 #include "Window.h"
97 #include "ShortcutHandler/ShortcutHandler.h"
99 #include "ui_CreateMailboxDialog.h"
100 #include "ui_AboutDialog.h"
102 #include "Imap/Model/ModelTest/modeltest.h"
103 #include "UiUtils/IconLoader.h"
104 #include "UiUtils/QaimDfsIterator.h"
106 /** @short All user-facing widgets and related classes */
107 namespace Gui
110 static const char * const netErrorUnseen = "net_error_unseen";
112 MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0),
113 m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0),
114 m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0)
116 setAttribute(Qt::WA_AlwaysShowToolTips);
117 // m_pluginManager must be created before calling createWidgets
118 m_pluginManager = new Plugins::PluginManager(this, m_settings,
119 Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin);
120 connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged);
121 connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) {
122 Gui::Util::messageBoxWarning(this, tr("Plugin Error"),
123 //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption.
124 trUtf8("A plugin failed to load, therefore some functionality might be lost. "
125 "You might want to update your system or report a bug to your vendor."
126 "\n\n%1").arg(errorMessage));
128 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
129 Plugins::PluginManager::MimePartReplacers replacers;
130 #ifdef TROJITA_HAVE_GPGMEPP
131 replacers.emplace_back(std::make_shared<Cryptography::GpgMeReplacer>());
132 #endif
133 m_pluginManager->setMimePartReplacers(replacers);
134 #endif
136 // ImapAccess contains a wrapper for retrieving passwords through some plugin.
137 // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class,
138 // which means that ImapAccess has to be constructed before we go and open the settings dialog.
140 // FIXME: use another account-id at some point in future
141 // we are now using the profile to avoid overwriting passwords of
142 // other profiles in secure storage
143 QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
144 m_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName);
145 connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError);
146 connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection);
148 ShortcutHandler *shortcutHandler = new ShortcutHandler(this);
149 shortcutHandler->setSettingsObject(m_settings);
150 defineActions();
151 shortcutHandler->readSettings(); // must happen after defineActions()
153 createWidgets();
155 Imap::migrateSettings(m_settings);
157 m_senderIdentities = new Composer::SenderIdentitiesModel(this);
158 m_senderIdentities->loadFromSettings(*m_settings);
160 if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) {
161 QTimer::singleShot(0, this, SLOT(slotShowSettings()));
165 setupModels();
166 createActions();
167 createMenus();
168 slotToggleSysTray();
169 slotPluginsChanged();
171 // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro
172 QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl");
173 QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact");
175 slotUpdateWindowTitle();
177 recoverDrafts();
179 if (m_actionLayoutWide->isEnabled() &&
180 m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) {
181 m_actionLayoutWide->trigger();
182 } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) {
183 m_actionLayoutOneAtTime->trigger();
184 } else {
185 m_actionLayoutCompact->trigger();
188 connect(qApp, &QGuiApplication::applicationStateChanged, this,
189 [&](Qt::ApplicationState state) {
190 if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) {
191 m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
192 m_networkErrorMessageBox->show();
196 // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but
197 // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time.
198 // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence
199 // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts
200 // to that and only after the panel gets resized, the available size of "the rest" is correct again).
201 // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially
202 // because every change bumps the timer anyway, as Thomas pointed out).
203 QTimer *delayedResize = new QTimer(this);
204 delayedResize->setSingleShot(true);
205 delayedResize->setInterval(500);
206 connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged);
207 connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast<void (QTimer::*)()>(&QTimer::start));
208 m_skipSavingOfUI = false;
211 void MainWindow::defineActions()
213 ShortcutHandler *shortcutHandler = ShortcutHandler::instance();
214 shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit);
215 shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New);
216 shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft..."));
217 shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M"));
218 shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E"));
219 shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M"));
220 shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N"));
221 shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P"));
222 shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString());
223 shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S"));
224 shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J"));
225 shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J"));
226 shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message..."));
227 shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source..."));
228 shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U"));
229 shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A"));
230 shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R"));
231 shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R"));
232 shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L"));
233 shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R"));
234 shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F"));
235 shortcutHandler->defineAction(QStringLiteral("action_archive"), QStringLiteral("mail-move-to-archive"), tr("&Archive"), QStringLiteral("A"));
236 shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book..."));
237 shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline"));
238 shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection"));
239 shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access"));
240 shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window"));
241 shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString());
242 shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn);
243 shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut);
244 shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size"));
245 shortcutHandler->defineAction(QStringLiteral("action_focus_mailbox_tree"), QString(), tr("Move Focus to Mailbox List"));
246 shortcutHandler->defineAction(QStringLiteral("action_focus_msg_list"), QString(), tr("Move Focus to Message List"));
249 void MainWindow::createActions()
251 // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default:
253 // Thunderbird:
254 // private: Ctrl+R
255 // all: Ctrl+Shift+R
256 // list: Ctrl+Shift+L
257 // forward: Ctrl+L
258 // (no shortcuts for type of forwarding)
259 // bounce: ctrl+B
260 // new message: Ctrl+N
262 // KMail:
263 // "reply": R
264 // private: Shift+A
265 // all: A
266 // list: L
267 // forward as attachment: F
268 // forward inline: Shift+F
269 // bounce: E
270 // new: Ctrl+N
272 m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this);
274 m_mainToolbar = addToolBar(tr("Navigation"));
275 m_mainToolbar->setObjectName(QStringLiteral("mainToolbar"));
277 reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this);
278 connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList);
280 resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this);
281 connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox);
283 reloadAllMailboxes = new QAction(tr("&Reload Everything"), this);
284 // connect later
286 exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this);
287 exitAction->setStatusTip(tr("Exit the application"));
289 netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline"));
290 netOffline->setCheckable(true);
291 // connect later
292 netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive"));
293 netExpensive->setCheckable(true);
294 // connect later
295 netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online"));
296 netOnline->setCheckable(true);
297 // connect later
299 QActionGroup *netPolicyGroup = new QActionGroup(this);
300 netPolicyGroup->setExclusive(true);
301 netPolicyGroup->addAction(netOffline);
302 netPolicyGroup->addAction(netExpensive);
303 netPolicyGroup->addAction(netOnline);
305 //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts
306 showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this);
307 showFullView->setCheckable(true);
308 connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible);
309 connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked);
311 //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox
312 showTaskView = new QAction(tr("Show ImapTask t&ree"), this);
313 showTaskView->setCheckable(true);
314 connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible);
315 connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked);
317 //: a debugging tool showing the mime tree of the current message
318 showMimeView = new QAction(tr("Show &MIME tree"), this);
319 showMimeView->setCheckable(true);
320 connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible);
321 connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked);
323 showImapLogger = new QAction(tr("Show IMAP protocol &log"), this);
324 showImapLogger->setCheckable(true);
325 connect(showImapLogger, &QAction::toggled, imapLoggerDock, &QWidget::setVisible);
326 connect(imapLoggerDock, &QDockWidget::visibilityChanged, showImapLogger, &QAction::setChecked);
328 //: file to save the debug log into
329 logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this);
330 logPersistent->setCheckable(true);
331 connect(logPersistent, &QAction::triggered, imapLogger, &ProtocolLoggerWidget::slotSetPersistentLogging);
332 connect(imapLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked);
334 showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this);
335 connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo);
337 showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this);
338 showMenuBar->setCheckable(true);
339 showMenuBar->setChecked(true);
340 connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible);
341 connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
343 showToolBar = new QAction(tr("Show &Toolbar"), this);
344 showToolBar->setCheckable(true);
345 connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible);
346 connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked);
347 connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
349 configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")), tr("&Settings..."), this);
350 connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings);
352 QAction *triggerSearch = new QAction(this);
353 addAction(triggerSearch);
354 triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, =")));
355 connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch);
357 triggerSearch = new QAction(this);
358 addAction(triggerSearch);
359 triggerSearch->setShortcut(QKeySequence(QStringLiteral("/")));
360 connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusSearch);
362 addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_mailbox_tree"), mboxTree,
363 SLOT(setFocus()), this));
364 addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_msg_list"), msgListWidget->tree,
365 SLOT(setFocus()), this));
367 m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this);
368 m_oneAtTimeGoBack->setEnabled(false);
370 composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this);
371 m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this);
373 expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this);
375 m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this);
376 markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this);
377 markAsRead->setCheckable(true);
378 msgListWidget->tree->addAction(markAsRead);
379 connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead);
381 m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this);
382 msgListWidget->tree->addAction(m_nextMessage);
383 m_messageWidget->messageView->addAction(m_nextMessage);
385 m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this);
386 msgListWidget->tree->addAction(m_previousMessage);
387 m_messageWidget->messageView->addAction(m_previousMessage);
389 markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this);
390 markAsDeleted->setCheckable(true);
391 msgListWidget->tree->addAction(markAsDeleted);
392 connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted);
394 markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this);
395 markAsFlagged->setCheckable(true);
396 connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged);
398 markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this);
399 markAsJunk->setCheckable(true);
400 connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk);
402 markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this);
403 markAsNotJunk->setCheckable(true);
404 connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk);
406 saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this);
407 msgListWidget->tree->addAction(saveWholeMessage);
409 viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this);
410 msgListWidget->tree->addAction(viewMsgSource);
412 viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this);
413 msgListWidget->tree->addAction(viewMsgHeaders);
415 moveToArchive = ShortcutHandler::instance()->createAction(QStringLiteral("action_archive"), this);
416 connect(moveToArchive, &QAction::triggered, this, &MainWindow::handleMoveToArchive);
418 //: "mailbox" as a "folder of messages", not as a "mail account"
419 createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this);
420 connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent);
422 //: "mailbox" as a "folder of messages", not as a "mail account"
423 createTopMailbox = new QAction(tr("Create &New Mailbox..."), this);
424 connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox);
426 m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this);
427 connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead);
429 //: "mailbox" as a "folder of messages", not as a "mail account"
430 deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this);
431 connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox);
433 #ifdef XTUPLE_CONNECT
434 xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this);
435 xtIncludeMailboxInSync->setCheckable(true);
436 connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox()));
437 #endif
439 m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this);
440 m_replyPrivate->setEnabled(false);
442 m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this);
443 m_replyAllButMe->setEnabled(false);
445 m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this);
446 m_replyAll->setEnabled(false);
448 m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this);
449 m_replyList->setEnabled(false);
451 m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this);
452 m_replyGuess->setEnabled(true);
454 actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this);
455 actionThreadMsgList->setCheckable(true);
456 // This action is enabled/disabled by model's capabilities
457 actionThreadMsgList->setEnabled(false);
458 if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) {
459 actionThreadMsgList->setChecked(true);
460 // The actual threading will be performed only when model updates its capabilities
462 connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList);
464 QActionGroup *sortOrderGroup = new QActionGroup(this);
465 m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup);
466 m_actionSortAscending->setCheckable(true);
467 m_actionSortAscending->setChecked(true);
468 m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup);
469 m_actionSortDescending->setCheckable(true);
470 // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-)
471 connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
472 connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent);
473 connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
475 QActionGroup *sortColumnGroup = new QActionGroup(this);
476 m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup);
477 m_actionSortNone->setCheckable(true);
478 m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup);
479 m_actionSortThreading->setCheckable(true);
480 m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup);
481 m_actionSortByArrival->setCheckable(true);
482 m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup);
483 m_actionSortByCc->setCheckable(true);
484 m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup);
485 m_actionSortByDate->setCheckable(true);
486 m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup);
487 m_actionSortByFrom->setCheckable(true);
488 m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup);
489 m_actionSortBySize->setCheckable(true);
490 m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup);
491 m_actionSortBySubject->setCheckable(true);
492 m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup);
493 m_actionSortByTo->setCheckable(true);
494 connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
495 slotSortingConfirmed(-1, Qt::AscendingOrder);
497 actionHideRead = new QAction(tr("&Hide Read Messages"), this);
498 actionHideRead->setCheckable(true);
499 if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) {
500 actionHideRead->setChecked(true);
501 prettyMsgListModel->setHideRead(true);
503 connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead);
505 QActionGroup *layoutGroup = new QActionGroup(this);
506 m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup);
507 m_actionLayoutCompact->setCheckable(true);
508 m_actionLayoutCompact->setChecked(true);
509 connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact);
510 m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup);
511 m_actionLayoutWide->setCheckable(true);
512 connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide);
513 m_actionLayoutOneAtTime = new QAction(tr("&One At Time"), layoutGroup);
514 m_actionLayoutOneAtTime->setCheckable(true);
515 connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime);
518 m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this);
519 m_actionShowOnlySubscribed->setCheckable(true);
520 m_actionShowOnlySubscribed->setEnabled(false);
521 connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed);
522 m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this);
523 m_actionSubscribeMailbox->setCheckable(true);
524 m_actionSubscribeMailbox->setEnabled(false);
525 connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox);
527 aboutTrojita = new QAction(trUtf8("&About Trojitá..."), this);
528 connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita);
530 donateToTrojita = new QAction(tr("&Donate to the project"), this);
531 connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita);
533 connectModelActions();
535 m_composeMenu = new QMenu(tr("Compose Mail"), this);
536 m_composeMenu->addAction(composeMail);
537 m_composeMenu->addAction(m_editDraft);
538 m_composeButton = new QToolButton(this);
539 m_composeButton->setPopupMode(QToolButton::MenuButtonPopup);
540 m_composeButton->setMenu(m_composeMenu);
541 m_composeButton->setDefaultAction(composeMail);
543 m_replyButton = new QToolButton(this);
544 m_replyButton->setPopupMode(QToolButton::MenuButtonPopup);
545 m_replyMenu = new QMenu(m_replyButton);
546 m_replyMenu->addAction(m_replyPrivate);
547 m_replyMenu->addAction(m_replyAllButMe);
548 m_replyMenu->addAction(m_replyAll);
549 m_replyMenu->addAction(m_replyList);
550 m_replyButton->setMenu(m_replyMenu);
551 m_replyButton->setDefaultAction(m_replyPrivate);
553 m_mainToolbar->addWidget(m_composeButton);
554 m_mainToolbar->addWidget(m_replyButton);
555 m_mainToolbar->addAction(m_forwardAsAttachment);
556 m_mainToolbar->addAction(expunge);
557 m_mainToolbar->addSeparator();
558 m_mainToolbar->addAction(markAsRead);
559 m_mainToolbar->addAction(markAsDeleted);
560 m_mainToolbar->addAction(markAsFlagged);
561 m_mainToolbar->addAction(markAsJunk);
562 m_mainToolbar->addAction(markAsNotJunk);
563 m_mainToolbar->addAction(moveToArchive);
565 // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom.
566 QWidget *toolbarSpacer = new QWidget(m_mainToolbar);
567 toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
568 m_mainToolbar->addWidget(toolbarSpacer);
570 m_mainToolbar->addSeparator();
571 m_mainToolbar->addWidget(busyParsersIndicator);
573 networkIndicator = new QToolButton(this);
574 // This is deliberate; we want to show this button in the same style as the other ones in the toolbar
575 networkIndicator->setPopupMode(QToolButton::MenuButtonPopup);
576 m_mainToolbar->addWidget(networkIndicator);
578 m_menuFromToolBar = new QToolButton(this);
579 m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new")));
580 m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON'
581 m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup);
582 connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu);
583 m_mainToolbar->addWidget(m_menuFromToolBar);
584 connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) {
585 // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton
586 m_mainToolbar->actions().last()->setVisible(!menuBarVisible);
588 m_mainToolbar->actions().last()->setVisible(false); // initial state to complement the default of the QMenuBar's visibility
590 busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize());
593 // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified.
594 // That sucks, because some of our widgets use multiple actions with an expanding arrow at right.
595 // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows
596 // are, well, at right.
597 // I have no idea how this works on RTL layouts.
598 QLayout *lay = m_mainToolbar->layout();
599 for (int i = 0; i < lay->count(); ++i) {
600 QLayoutItem *it = lay->itemAt(i);
601 if (it->widget() == toolbarSpacer) {
602 // Don't align this one, otherwise it won't push stuff when in horizontal direction
603 continue;
605 if (it->widget() == busyParsersIndicator) {
606 // It looks much better when centered
607 it->setAlignment(Qt::AlignJustify);
608 continue;
610 it->setAlignment(Qt::AlignLeft);
614 updateMessageFlags();
617 void MainWindow::connectModelActions()
619 connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList);
620 connect(netOffline, &QAction::triggered,
621 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline);
622 connect(netExpensive, &QAction::triggered,
623 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive);
624 connect(netOnline, &QAction::triggered,
625 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline);
626 netExpensive->setEnabled(imapAccess()->isConfigured());
627 netOnline->setEnabled(imapAccess()->isConfigured());
630 void MainWindow::createMenus()
632 #define ADD_ACTION(MENU, ACTION) \
633 MENU->addAction(ACTION); \
634 addAction(ACTION);
636 QMenu *imapMenu = menuBar()->addMenu(tr("&IMAP"));
637 imapMenu->addMenu(m_composeMenu);
638 ADD_ACTION(imapMenu, m_actionContactEditor);
639 ADD_ACTION(imapMenu, m_replyGuess);
640 ADD_ACTION(imapMenu, m_replyPrivate);
641 ADD_ACTION(imapMenu, m_replyAll);
642 ADD_ACTION(imapMenu, m_replyAllButMe);
643 ADD_ACTION(imapMenu, m_replyList);
644 imapMenu->addSeparator();
645 ADD_ACTION(imapMenu, m_forwardAsAttachment);
646 imapMenu->addSeparator();
647 ADD_ACTION(imapMenu, expunge);
648 imapMenu->addSeparator()->setText(tr("Network Access"));
649 QMenu *netPolicyMenu = imapMenu->addMenu(tr("&Network Access"));
650 ADD_ACTION(netPolicyMenu, netOffline);
651 ADD_ACTION(netPolicyMenu, netExpensive);
652 ADD_ACTION(netPolicyMenu, netOnline);
653 QMenu *debugMenu = imapMenu->addMenu(tr("&Debugging"));
654 ADD_ACTION(debugMenu, showFullView);
655 ADD_ACTION(debugMenu, showTaskView);
656 ADD_ACTION(debugMenu, showMimeView);
657 ADD_ACTION(debugMenu, showImapLogger);
658 ADD_ACTION(debugMenu, logPersistent);
659 ADD_ACTION(debugMenu, showImapCapabilities);
660 imapMenu->addSeparator();
661 ADD_ACTION(imapMenu, configSettings);
662 ADD_ACTION(imapMenu, ShortcutHandler::instance()->shortcutConfigAction());
663 imapMenu->addSeparator();
664 ADD_ACTION(imapMenu, exitAction);
666 QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
667 ADD_ACTION(viewMenu, showMenuBar);
668 ADD_ACTION(viewMenu, showToolBar);
669 QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout"));
670 ADD_ACTION(layoutMenu, m_actionLayoutCompact);
671 ADD_ACTION(layoutMenu, m_actionLayoutWide);
672 ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime);
673 viewMenu->addSeparator();
674 ADD_ACTION(viewMenu, m_previousMessage);
675 ADD_ACTION(viewMenu, m_nextMessage);
676 viewMenu->addSeparator();
677 QMenu *sortMenu = viewMenu->addMenu(tr("S&orting"));
678 ADD_ACTION(sortMenu, m_actionSortNone);
679 ADD_ACTION(sortMenu, m_actionSortThreading);
680 ADD_ACTION(sortMenu, m_actionSortByArrival);
681 ADD_ACTION(sortMenu, m_actionSortByCc);
682 ADD_ACTION(sortMenu, m_actionSortByDate);
683 ADD_ACTION(sortMenu, m_actionSortByFrom);
684 ADD_ACTION(sortMenu, m_actionSortBySize);
685 ADD_ACTION(sortMenu, m_actionSortBySubject);
686 ADD_ACTION(sortMenu, m_actionSortByTo);
687 sortMenu->addSeparator();
688 ADD_ACTION(sortMenu, m_actionSortAscending);
689 ADD_ACTION(sortMenu, m_actionSortDescending);
691 ADD_ACTION(viewMenu, actionThreadMsgList);
692 ADD_ACTION(viewMenu, actionHideRead);
693 ADD_ACTION(viewMenu, m_actionShowOnlySubscribed);
695 QMenu *mailboxMenu = menuBar()->addMenu(tr("&Mailbox"));
696 ADD_ACTION(mailboxMenu, resyncMbox);
697 mailboxMenu->addSeparator();
698 ADD_ACTION(mailboxMenu, reloadAllMailboxes);
700 QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
701 ADD_ACTION(helpMenu, donateToTrojita);
702 helpMenu->addSeparator();
703 ADD_ACTION(helpMenu, aboutTrojita);
705 QMenu *mainMenuBehindToolBar = new QMenu(this);
706 m_menuFromToolBar->setMenu(mainMenuBehindToolBar);
707 m_menuFromToolBar->menu()->addMenu(imapMenu);
708 m_menuFromToolBar->menu()->addMenu(viewMenu);
709 m_menuFromToolBar->menu()->addMenu(mailboxMenu);
710 m_menuFromToolBar->menu()->addMenu(helpMenu);
711 m_menuFromToolBar->menu()->addSeparator();
712 m_menuFromToolBar->menu()->addAction(showMenuBar);
714 networkIndicator->setMenu(netPolicyMenu);
715 m_netToolbarDefaultAction = new QAction(this);
716 networkIndicator->setDefaultAction(m_netToolbarDefaultAction);
717 connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu);
718 connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
719 connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
720 connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
722 #undef ADD_ACTION
725 void MainWindow::createWidgets()
727 // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure
728 // we do not hit the disk continually when e.g. resizing some random widget.
729 m_delayedStateSaving = new QTimer(this);
730 m_delayedStateSaving->setInterval(1000);
731 m_delayedStateSaving->setSingleShot(true);
732 connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState);
734 mboxTree = new MailBoxTreeView();
735 mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList());
736 connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree);
737 connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) {
738 m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames);
741 msgListWidget = new MessageListWidget();
742 msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu);
743 msgListWidget->tree->setAlternatingRowColors(true);
744 msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool());
745 connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting);
747 connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree);
748 connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked);
749 connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked);
750 connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::msgListDoubleClicked);
751 connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested);
752 connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
753 connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
755 msgListWidget->tree->installEventFilter(this);
757 m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager);
758 connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp);
759 connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions);
760 #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
761 connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) {
762 if (url.isEmpty()) {
763 QToolTip::hideText();
764 } else {
765 // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783
766 QTimer::singleShot(250, [url]() {
767 QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url)));
771 #endif
772 connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError);
773 // Do not try to get onto the homepage when we are on EXPENSIVE connection
774 if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() &&
775 m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) {
776 m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version)));
779 allDock = new QDockWidget(tr("Everything"), this);
780 allDock->setObjectName(QStringLiteral("allDock"));
781 allTree = new QTreeView(allDock);
782 allDock->hide();
783 allTree->setUniformRowHeights(true);
784 allTree->setHeaderHidden(true);
785 allDock->setWidget(allTree);
786 addDockWidget(Qt::LeftDockWidgetArea, allDock);
787 taskDock = new QDockWidget(tr("IMAP Tasks"), this);
788 taskDock->setObjectName(QStringLiteral("taskDock"));
789 taskTree = new QTreeView(taskDock);
790 taskDock->hide();
791 taskTree->setHeaderHidden(true);
792 taskDock->setWidget(taskTree);
793 addDockWidget(Qt::LeftDockWidgetArea, taskDock);
794 mailMimeDock = new QDockWidget(tr("MIME Tree"), this);
795 mailMimeDock->setObjectName(QStringLiteral("mailMimeDock"));
796 mailMimeTree = new QTreeView(mailMimeDock);
797 mailMimeDock->hide();
798 mailMimeTree->setUniformRowHeights(true);
799 mailMimeTree->setHeaderHidden(true);
800 mailMimeDock->setWidget(mailMimeTree);
801 addDockWidget(Qt::RightDockWidgetArea, mailMimeDock);
802 connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged);
804 imapLoggerDock = new QDockWidget(tr("IMAP Protocol"), this);
805 imapLoggerDock->setObjectName(QStringLiteral("imapLoggerDock"));
806 imapLogger = new ProtocolLoggerWidget(imapLoggerDock);
807 imapLoggerDock->hide();
808 imapLoggerDock->setWidget(imapLogger);
809 addDockWidget(Qt::BottomDockWidgetArea, imapLoggerDock);
811 busyParsersIndicator = new TaskProgressIndicator(this);
814 void MainWindow::setupModels()
816 m_imapAccess->reloadConfiguration();
817 m_imapAccess->doConnect();
819 m_messageWidget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
821 auto realThreadingModel = qobject_cast<Imap::Mailbox::ThreadingMsgListModel*>(m_imapAccess->threadingMsgListModel());
822 Q_ASSERT(realThreadingModel);
823 auto realMsgListModel = qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel());
824 Q_ASSERT(realMsgListModel);
826 prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast<QAbstractItemModel *>(m_imapAccess->mailboxModel()));
827 prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel"));
828 connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed,
829 msgListWidget, &MessageListWidget::slotSortingFailed);
830 prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this);
831 prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel());
832 prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel"));
834 connect(mboxTree, &MailBoxTreeView::clicked,
835 realMsgListModel,
836 static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
837 connect(mboxTree, &MailBoxTreeView::activated,
838 realMsgListModel,
839 static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
840 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags);
841 connect(qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable,
842 this, &MainWindow::slotScrollToUnseenMessage);
843 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
844 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
845 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags);
846 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
847 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags);
848 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
849 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags);
850 connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged);
852 connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived);
853 connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError);
854 connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError);
855 connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection);
856 connect(imapModel(), &Imap::Mailbox::Model::authAttemptFailed, this, [this]() {
857 m_ignoreStoredPassword = true;
860 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline);
861 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive);
862 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline);
863 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, [this](uint, const Imap::ConnectionState state) {
864 if (state == Imap::CONN_STATE_AUTHENTICATED) {
865 m_ignoreStoredPassword = false;
868 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus);
870 connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed);
871 connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed);
872 connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed);
874 connect(imapModel(), &Imap::Mailbox::Model::logged, imapLogger, &ProtocolLoggerWidget::slotImapLogged);
875 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, imapLogger, &ProtocolLoggerWidget::onConnectionClosed);
877 auto nw = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher());
878 Q_ASSERT(nw);
879 connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled,
880 this, [this](const int timeout) {
881 showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000));
883 connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState);
885 connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage);
887 connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated);
889 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle);
890 connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle);
892 connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed);
894 //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this );
895 //w->setModel( imapModel() );
897 //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time
899 mboxTree->setModel(prettyMboxModel);
900 msgListWidget->tree->setModel(prettyMsgListModel);
901 connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags);
903 allTree->setModel(imapModel());
904 taskTree->setModel(imapModel()->taskModel());
905 connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll);
906 connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll);
907 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll);
908 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll);
909 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll);
911 busyParsersIndicator->setImapModel(imapModel());
913 auto accountIconName = m_settings->value(Common::SettingsNames::imapAccountIcon).toString();
914 if (accountIconName.isEmpty()) {
915 qApp->setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita")));
916 } else if (accountIconName.contains(QDir::separator())) {
917 // Absolute paths are OK for users, but unsupported by our icon loader
918 qApp->setWindowIcon(QIcon(accountIconName));
919 } else {
920 qApp->setWindowIcon(UiUtils::loadIcon(accountIconName));
924 void MainWindow::createSysTray()
926 if (m_trayIcon)
927 return;
929 qApp->setQuitOnLastWindowClosed(false);
931 m_trayIcon = new QSystemTrayIcon(this);
932 handleTrayIconChange();
934 QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon);
935 connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
937 QMenu *trayIconMenu = new QMenu(this);
938 trayIconMenu->addAction(quitAction);
939 m_trayIcon->setContextMenu(trayIconMenu);
941 // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope.
942 connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater);
944 connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated);
945 connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange);
946 m_trayIcon->setVisible(true);
947 m_trayIcon->show();
950 void MainWindow::removeSysTray()
952 delete m_trayIcon;
953 m_trayIcon = 0;
955 qApp->setQuitOnLastWindowClosed(true);
958 void MainWindow::slotToggleSysTray()
960 bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool();
961 if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) {
962 createSysTray();
963 } else if (!showSystray && m_trayIcon) {
964 removeSysTray();
968 void MainWindow::handleTrayIconChange()
970 if (!m_trayIcon)
971 return;
973 const bool isOffline = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher())->effectiveNetworkPolicy()
974 == Imap::Mailbox::NETWORK_OFFLINE;
975 auto pixmap = qApp->windowIcon()
976 .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal);
977 QString tooltip;
978 auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
979 if (profileName.isEmpty()) {
980 tooltip = QStringLiteral("Trojitá");
981 } else {
982 tooltip = QStringLiteral("Trojitá [%1]").arg(profileName);
985 uint unreadCount = 0;
986 bool numbersValid = false;
988 auto watchingMode = settings()->value(Common::SettingsNames::watchedFoldersKey).toString();
989 if (watchingMode == Common::SettingsNames::watchAll || watchingMode == Common::SettingsNames::watchSubscribed) {
990 bool subscribedOnly = watchingMode == Common::SettingsNames::watchSubscribed;
991 unreadCount = std::accumulate(UiUtils::QaimDfsIterator(m_imapAccess->mailboxModel()->index(0, 0)),
992 UiUtils::QaimDfsIterator(), 0, [subscribedOnly](const uint acc, const QModelIndex &idx) {
994 if (subscribedOnly && !idx.data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool())
995 return acc;
997 auto x = idx.data(Imap::Mailbox::RoleUnreadMessageCount).toInt();
998 if (x > 0) {
999 return acc + x;
1000 } else {
1001 return acc;
1004 // only show stuff if there are some mailboxes, and if there are such messages
1005 numbersValid = m_imapAccess->mailboxModel()->hasChildren() && unreadCount > 0;
1007 } else {
1008 // just for the INBOX
1009 QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex());
1010 if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX")
1011 && mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) {
1012 unreadCount = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt();
1013 numbersValid = true;
1017 if (numbersValid) {
1018 QFont f;
1019 f.setPixelSize(pixmap.height() * 0.59);
1020 f.setWeight(QFont::Bold);
1022 QString text = QString::number(unreadCount);
1023 QFontMetrics fm(f);
1024 if (unreadCount > 666) {
1025 // You just have too many messages.
1026 text = QStringLiteral("🐮");
1027 fm = QFontMetrics(f);
1028 } else if (fm.width(text) > pixmap.width()) {
1029 f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text));
1030 fm = QFontMetrics(f);
1033 QRect boundingRect = fm.tightBoundingRect(text);
1034 boundingRect.setWidth(boundingRect.width() + 2);
1035 boundingRect.setHeight(boundingRect.height() + 2);
1036 boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2));
1037 boundingRect = boundingRect.intersected(pixmap.rect());
1039 QPainterPath path;
1040 path.addText(boundingRect.bottomLeft(), f, text);
1042 QPainter painter(&pixmap);
1043 painter.setRenderHint(QPainter::Antialiasing);
1044 painter.setPen(QColor(255,255,255, 180));
1045 painter.setBrush(isOffline ? Qt::red : Qt::black);
1046 painter.drawPath(path);
1048 //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]"
1049 tooltip += trUtf8(" - %n unread message(s)", 0, unreadCount);
1050 } else if (isOffline) {
1051 //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]"
1052 tooltip += tr(" - offline");
1054 m_trayIcon->setToolTip(tooltip);
1055 m_trayIcon->setIcon(QIcon(pixmap));
1058 void MainWindow::closeEvent(QCloseEvent *event)
1060 if (m_trayIcon && m_trayIcon->isVisible()) {
1061 Util::askForSomethingUnlessTold(trUtf8("Trojitá"),
1062 tr("The application will continue in systray. This can be disabled within the settings."),
1063 Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings);
1064 hide();
1065 event->ignore();
1069 bool MainWindow::eventFilter(QObject *o, QEvent *e)
1071 if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) {
1072 if (e->type() == QEvent::KeyPress) {
1073 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
1074 if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) {
1075 QCoreApplication::sendEvent(m_messageWidget, keyEvent);
1076 return true;
1078 return false;
1080 return false;
1082 if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) {
1083 // installed if sorting is not really possible.
1084 QWidget *header = static_cast<QWidget*>(o);
1085 QMouseEvent *mouse = static_cast<QMouseEvent*>(e);
1086 if (e->type() == QEvent::MouseButtonPress) {
1087 if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) {
1088 m_headerDragStart = mouse->pos();
1090 return false;
1092 if (e->type() == QEvent::MouseButtonRelease) {
1093 if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor &&
1094 (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) {
1095 m_actionSortDescending->toggle();
1096 Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
1097 msgListWidget->tree->header()->setSortIndicator(-1, order);
1098 return true; // prevent regular click
1102 return false;
1105 void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason)
1107 if (reason == QSystemTrayIcon::Trigger) {
1108 setVisible(!isVisible());
1109 if (isVisible())
1110 showMainWindow();
1114 void MainWindow::showMainWindow()
1116 setVisible(true);
1117 activateWindow();
1118 raise();
1121 void MainWindow::msgListClicked(const QModelIndex &index)
1123 Q_ASSERT(index.isValid());
1125 if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier)
1126 return;
1128 if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1129 return;
1131 // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that
1132 // user will be notified about new arrivals, etc.
1133 QModelIndex translated = Imap::deproxifiedIndex(index);
1134 imapModel()->switchToMailbox(translated.parent().parent());
1136 if (index.column() == Imap::Mailbox::MsgListModel::SEEN) {
1137 if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1138 return;
1139 Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ?
1140 Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1141 imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp);
1143 if (translated == m_messageWidget->messageView->currentMessage()) {
1144 m_messageWidget->messageView->stopAutoMarkAsRead();
1146 } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) {
1147 if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1148 return;
1150 Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ?
1151 Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1152 imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp);
1153 } else {
1154 if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) {
1155 // isVisible() won't work, the splitter manipulates width, not the visibility state
1156 m_messageWidget->messageView->setMessage(index);
1158 msgListWidget->tree->setCurrentIndex(index);
1162 void MainWindow::msgListDoubleClicked(const QModelIndex &index)
1164 Q_ASSERT(index.isValid());
1166 if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1167 return;
1169 CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager);
1170 widget->messageView->setMessage(index);
1171 widget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
1172 widget->setFocusPolicy(Qt::StrongFocus);
1173 widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString());
1174 widget->setAttribute(Qt::WA_DeleteOnClose);
1175 QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget);
1176 widget->addAction(closeAction);
1177 widget->show();
1180 void MainWindow::showContextMenuMboxTree(const QPoint &position)
1182 QList<QAction *> actionList;
1183 if (mboxTree->indexAt(position).isValid()) {
1184 actionList.append(createChildMailbox);
1185 actionList.append(deleteCurrentMailbox);
1186 actionList.append(m_actionMarkMailboxAsRead);
1187 actionList.append(resyncMbox);
1188 actionList.append(reloadMboxList);
1190 actionList.append(m_actionSubscribeMailbox);
1191 m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool());
1193 #ifdef XTUPLE_CONNECT
1194 actionList.append(xtIncludeMailboxInSync);
1195 xtIncludeMailboxInSync->setChecked(
1196 m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains(
1197 mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString()));
1198 #endif
1199 } else {
1200 actionList.append(createTopMailbox);
1202 actionList.append(reloadAllMailboxes);
1203 actionList.append(m_actionShowOnlySubscribed);
1204 QMenu::exec(actionList, mboxTree->mapToGlobal(position));
1207 void MainWindow::showContextMenuMsgListTree(const QPoint &position)
1209 QList<QAction *> actionList;
1210 QModelIndex index = msgListWidget->tree->indexAt(position);
1211 if (index.isValid()) {
1212 updateMessageFlagsOf(index);
1213 actionList.append(markAsRead);
1214 actionList.append(markAsDeleted);
1215 actionList.append(markAsFlagged);
1216 actionList.append(markAsJunk);
1217 actionList.append(markAsNotJunk);
1218 actionList.append(moveToArchive);
1219 actionList.append(m_actionMarkMailboxAsRead);
1220 actionList.append(saveWholeMessage);
1221 actionList.append(viewMsgSource);
1222 actionList.append(viewMsgHeaders);
1224 if (! actionList.isEmpty())
1225 QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position));
1228 /** @short Ask for an updated list of mailboxes situated below the selected one
1231 void MainWindow::slotReloadMboxList()
1233 Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1234 Q_ASSERT(item.isValid());
1235 if (item.column() != 0)
1236 continue;
1237 Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1238 Imap::Mailbox::Model::realTreeItem(item)
1240 Q_ASSERT(mbox);
1241 mbox->rescanForChildMailboxes(imapModel());
1245 /** @short Request a check for new messages in selected mailbox */
1246 void MainWindow::slotResyncMbox()
1248 if (! imapModel()->isNetworkAvailable())
1249 return;
1251 Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1252 Q_ASSERT(item.isValid());
1253 if (item.column() != 0)
1254 continue;
1255 imapModel()->resyncMailbox(item);
1259 void MainWindow::alertReceived(const QString &message)
1261 //: "ALERT" is a special warning which we're required to show to the user
1262 Gui::Util::messageBoxWarning(this, tr("IMAP Alert"), message);
1265 void MainWindow::imapError(const QString &message)
1267 Gui::Util::messageBoxCritical(this, tr("IMAP Protocol Error"), message);
1268 // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report.
1269 // </joke>
1270 showImapLogger->setChecked(true);
1273 void MainWindow::networkError(const QString &message)
1275 const QString title = tr("Network Error");
1276 if (!m_networkErrorMessageBox) {
1277 m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title,
1278 QString(), QMessageBox::Ok, this);
1280 // User must be informed about a new (but not recurring) error
1281 if (message != m_networkErrorMessageBox->text()) {
1282 m_networkErrorMessageBox->setText(message);
1283 if (qApp->applicationState() == Qt::ApplicationActive) {
1284 m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
1285 m_networkErrorMessageBox->show();
1286 } else {
1287 m_networkErrorMessageBox->setProperty(netErrorUnseen, true);
1288 if (m_trayIcon && m_trayIcon->isVisible())
1289 m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333);
1294 void MainWindow::cacheError(const QString &message)
1296 Gui::Util::messageBoxCritical(this, tr("IMAP Cache Error"),
1297 tr("The caching subsystem managing a cache of the data already "
1298 "downloaded from the IMAP server is having troubles. "
1299 "All caching will be disabled.\n\n%1").arg(message));
1302 void MainWindow::networkPolicyOffline()
1304 netExpensive->setChecked(false);
1305 netOnline->setChecked(false);
1306 netOffline->setChecked(true);
1307 updateActionsOnlineOffline(false);
1308 showStatusMessage(tr("Offline"));
1309 handleTrayIconChange();
1312 void MainWindow::networkPolicyExpensive()
1314 netOffline->setChecked(false);
1315 netOnline->setChecked(false);
1316 netExpensive->setChecked(true);
1317 updateActionsOnlineOffline(true);
1318 handleTrayIconChange();
1321 void MainWindow::networkPolicyOnline()
1323 netOffline->setChecked(false);
1324 netExpensive->setChecked(false);
1325 netOnline->setChecked(true);
1326 updateActionsOnlineOffline(true);
1327 handleTrayIconChange();
1330 /** @short Deletes a network error message box instance upon resetting of reconnect state */
1331 void MainWindow::slotResetReconnectState()
1333 if (m_networkErrorMessageBox) {
1334 delete m_networkErrorMessageBox;
1335 m_networkErrorMessageBox = 0;
1339 void MainWindow::slotShowSettings()
1341 SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_settings);
1342 if (dialog->exec() == QDialog::Accepted) {
1343 // FIXME: wipe cache in case we're moving between servers
1344 nukeModels();
1345 setupModels();
1346 connectModelActions();
1347 // The systray is still connected to the old model -- got to make sure it's getting updated
1348 removeSysTray();
1349 slotToggleSysTray();
1351 QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString();
1352 if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL &&
1353 method != Common::SettingsNames::methodProcess ) {
1354 Gui::Util::messageBoxCritical(this, tr("No Configuration"),
1355 trUtf8("No IMAP account is configured. Trojitá cannot do much without one."));
1357 applySizesAndState();
1360 void MainWindow::authenticationRequested()
1362 Plugins::PasswordPlugin *password = pluginManager()->password();
1363 if (password) {
1364 // FIXME: use another account-id at some point in future
1365 // Currently the accountName will be empty unless Trojita has been
1366 // called with a profile, and then the profile will be used as the
1367 // accountName.
1368 QString accountName = m_imapAccess->accountName();
1369 if (accountName.isEmpty())
1370 accountName = QStringLiteral("account-0");
1371 Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap"));
1372 if (job) {
1373 connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) {
1374 authenticationContinue(password);
1376 connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) {
1377 if (error == Plugins::PasswordJob::Error::NoSuchPassword) {
1378 authenticationContinue(QString());
1379 } else {
1380 authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message));
1383 job->setAutoDelete(true);
1384 job->start();
1385 return;
1389 authenticationContinue(QString());
1393 void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage)
1395 const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString();
1396 QString pass = password;
1397 if (m_ignoreStoredPassword || pass.isEmpty()) {
1398 auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"),
1399 tr("<p>Please provide IMAP password for user <b>%1</b> on <b>%2</b>:</p>").arg(
1400 user.toHtmlEscaped(),
1401 m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped()
1403 errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n"))
1404 + imapModel()->imapAuthError());
1405 connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword);
1406 connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword);
1407 } else {
1408 imapModel()->setImapPassword(pass);
1412 void MainWindow::checkSslPolicy()
1414 m_imapAccess->setSslPolicy(QMessageBox(static_cast<QMessageBox::Icon>(m_imapAccess->sslInfoIcon()),
1415 m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(),
1416 QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes);
1419 void MainWindow::nukeModels()
1421 m_messageWidget->messageView->setEmpty();
1422 mboxTree->setModel(0);
1423 msgListWidget->tree->setModel(0);
1424 allTree->setModel(0);
1425 taskTree->setModel(0);
1426 delete prettyMsgListModel;
1427 prettyMsgListModel = 0;
1428 delete prettyMboxModel;
1429 prettyMboxModel = 0;
1432 void MainWindow::recoverDrafts()
1434 QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/"));
1435 QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft")));
1436 Q_FOREACH(const QString &draft, drafts) {
1437 ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this);
1438 // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured
1439 if (!w)
1440 break;
1444 void MainWindow::slotComposeMail()
1446 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this);
1449 void MainWindow::slotEditDraft()
1451 QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts"));
1452 QDir().mkpath(path);
1453 path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)"));
1454 if (!path.isNull()) {
1455 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this);
1459 QModelIndexList MainWindow::translatedSelection() const
1461 QModelIndexList translatedIndexes;
1462 QModelIndexList selected = msgListWidget->tree->selectionModel()->selectedIndexes();
1463 const int originalItems = selected.length(); // only check collapsed/expanded status on original selection
1464 for (int i = 0; i < selected.length(); ++i) {
1465 const QModelIndex item = selected[i];
1466 if (item.column() != 0 || !item.data(Imap::Mailbox::RoleMessageUid).isValid())
1467 continue;
1468 translatedIndexes << Imap::deproxifiedIndex(item);
1469 // Now see if this is a collapsed thread and include all the collapsed items as needed
1470 // Also note that this is recursive - each child found is run through this same item loop for validity/child checks as well
1471 if (i >= originalItems || !msgListWidget->tree->isExpanded(item)) {
1472 for (int j = 0; j < item.model()->rowCount(item); ++j) {
1473 selected << item.child(j, 0); // Make sure this is run through the main loop as well - don't add it directly
1477 return translatedIndexes;
1480 void MainWindow::handleMarkAsRead(bool value)
1482 const QModelIndexList translatedIndexes = translatedSelection();
1483 if (translatedIndexes.isEmpty()) {
1484 qDebug() << "Model::handleMarkAsRead: no valid messages";
1485 } else {
1486 imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1487 if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) {
1488 m_messageWidget->messageView->stopAutoMarkAsRead();
1493 void MainWindow::slotNextUnread()
1495 QModelIndex current = msgListWidget->tree->currentIndex();
1497 UiUtils::gotoNext(msgListWidget->tree->model(), current,
1498 [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); },
1499 [this](const QModelIndex &idx) {
1500 Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool());
1501 m_messageWidget->messageView->setMessage(idx);
1502 msgListWidget->tree->setCurrentIndex(idx);
1504 []() {
1505 // nothing to do
1509 void MainWindow::slotPreviousUnread()
1511 QModelIndex current = msgListWidget->tree->currentIndex();
1513 UiUtils::gotoPrevious(msgListWidget->tree->model(), current,
1514 [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); },
1515 [this](const QModelIndex &idx) {
1516 Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool());
1517 m_messageWidget->messageView->setMessage(idx);
1518 msgListWidget->tree->setCurrentIndex(idx);
1520 []() {
1521 // nothing to do
1525 void MainWindow::handleMarkAsDeleted(bool value)
1527 const QModelIndexList translatedIndexes = translatedSelection();
1528 if (translatedIndexes.isEmpty()) {
1529 qDebug() << "Model::handleMarkAsDeleted: no valid messages";
1530 } else {
1531 imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1535 void MainWindow::handleMarkAsFlagged(const bool value)
1537 const QModelIndexList translatedIndexes = translatedSelection();
1538 if (translatedIndexes.isEmpty()) {
1539 qDebug() << "Model::handleMarkAsFlagged: no valid messages";
1540 } else {
1541 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1545 void MainWindow::handleMarkAsJunk(const bool value)
1547 const QModelIndexList translatedIndexes = translatedSelection();
1548 if (translatedIndexes.isEmpty()) {
1549 qDebug() << "Model::handleMarkAsJunk: no valid messages";
1550 } else {
1551 if (value) {
1552 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE);
1554 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1558 void MainWindow::handleMarkAsNotJunk(const bool value)
1560 const QModelIndexList translatedIndexes = translatedSelection();
1561 if (translatedIndexes.isEmpty()) {
1562 qDebug() << "Model::handleMarkAsNotJunk: no valid messages";
1563 } else {
1564 if (value) {
1565 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE);
1567 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1571 void MainWindow::slotMoveToArchiveFailed(const QString &error)
1573 // XXX disable busy cursor
1574 QMessageBox::critical(this, tr("Failed to archive"), error);
1577 void MainWindow::handleMoveToArchive()
1579 const QModelIndexList translatedIndexes = translatedSelection();
1580 if (translatedIndexes.isEmpty()) {
1581 qDebug() << "Model::handleMoveToArchive: no valid messages";
1582 } else {
1583 auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString();
1584 auto copyMoveMessagesTask = imapModel()->copyMoveMessages(
1585 archiveFolderName.isEmpty() ? Common::SettingsNames::imapDefaultArchiveFolderName : archiveFolderName,
1586 translatedIndexes, Imap::Mailbox::CopyMoveOperation::MOVE);
1587 connect(copyMoveMessagesTask, &Imap::Mailbox::ImapTask::failed, this, &MainWindow::slotMoveToArchiveFailed);
1592 void MainWindow::slotExpunge()
1594 imapModel()->expungeMailbox(qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox());
1597 void MainWindow::slotMarkCurrentMailboxRead()
1599 imapModel()->markMailboxAsRead(mboxTree->currentIndex());
1602 void MainWindow::slotCreateMailboxBelowCurrent()
1604 createMailboxBelow(mboxTree->currentIndex());
1607 void MainWindow::slotCreateTopMailbox()
1609 createMailboxBelow(QModelIndex());
1612 void MainWindow::createMailboxBelow(const QModelIndex &index)
1614 Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ?
1615 dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1616 Imap::Mailbox::Model::realTreeItem(index)) :
1619 Ui::CreateMailboxDialog ui;
1620 QDialog *dialog = new QDialog(this);
1621 ui.setupUi(dialog);
1623 dialog->setWindowTitle(mboxPtr ?
1624 tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) :
1625 tr("Create a Top-level Mailbox"));
1627 if (dialog->exec() == QDialog::Accepted) {
1628 QStringList parts;
1629 if (mboxPtr)
1630 parts << mboxPtr->mailbox();
1631 parts << ui.mailboxName->text();
1632 if (ui.otherMailboxes->isChecked())
1633 parts << QString();
1634 QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString()); // FIXME: top-level separator
1635 imapModel()->createMailbox(targetName,
1636 ui.subscribe->isChecked() ?
1637 Imap::Mailbox::AutoSubscription::SUBSCRIBE :
1638 Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION
1643 void MainWindow::slotDeleteCurrentMailbox()
1645 if (! mboxTree->currentIndex().isValid())
1646 return;
1648 QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex());
1649 Q_ASSERT(mailbox.isValid());
1650 QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString();
1652 if (QMessageBox::question(this, tr("Delete Mailbox"),
1653 tr("Are you sure to delete mailbox %1?").arg(name),
1654 QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
1655 imapModel()->deleteMailbox(name);
1659 void MainWindow::updateMessageFlags()
1661 updateMessageFlagsOf(QModelIndex());
1664 void MainWindow::updateMessageFlagsOf(const QModelIndex &index)
1666 QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection();
1667 const bool isValid = !indexes.isEmpty() &&
1668 // either we operate on the -already valided- selection or the index must be valid
1669 (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0);
1670 const bool okToModify = imapModel()->isNetworkAvailable() && isValid;
1672 markAsRead->setEnabled(okToModify);
1673 markAsDeleted->setEnabled(okToModify);
1674 markAsFlagged->setEnabled(okToModify);
1675 markAsJunk->setEnabled(okToModify);
1676 markAsNotJunk->setEnabled(okToModify);
1678 // There's no point in moving from Archive to, well, Archive
1679 auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString();
1680 if (archiveFolderName.isEmpty()) {
1681 archiveFolderName = Common::SettingsNames::imapDefaultArchiveFolderName;
1683 moveToArchive->setEnabled(okToModify &&
1684 std::any_of(indexes.cbegin(), indexes.cend(),
1685 [archiveFolderName](const QModelIndex &i) {
1686 return i.data(Imap::Mailbox::RoleMailboxName) != archiveFolderName;
1687 }));
1689 bool isRead = isValid,
1690 isDeleted = isValid,
1691 isFlagged = isValid,
1692 isJunk = isValid,
1693 isNotJunk = isValid;
1694 Q_FOREACH (const QModelIndex &i, indexes) {
1695 #define UPDATE_STATE(PROP) \
1696 if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \
1697 is##PROP = false;
1698 UPDATE_STATE(Read)
1699 UPDATE_STATE(Deleted)
1700 UPDATE_STATE(Flagged)
1701 UPDATE_STATE(Junk)
1702 UPDATE_STATE(NotJunk)
1703 #undef UPDATE_STATE
1705 markAsRead->setChecked(isRead);
1706 markAsDeleted->setChecked(isDeleted);
1707 markAsFlagged->setChecked(isFlagged);
1708 markAsJunk->setChecked(isJunk && !isNotJunk);
1709 markAsNotJunk->setChecked(isNotJunk && !isJunk);
1712 void MainWindow::updateActionsOnlineOffline(bool online)
1714 reloadMboxList->setEnabled(online);
1715 resyncMbox->setEnabled(online);
1716 expunge->setEnabled(online);
1717 createChildMailbox->setEnabled(online);
1718 createTopMailbox->setEnabled(online);
1719 deleteCurrentMailbox->setEnabled(online);
1720 m_actionMarkMailboxAsRead->setEnabled(online);
1721 updateMessageFlags();
1722 showImapCapabilities->setEnabled(online);
1723 if (!online) {
1724 m_replyGuess->setEnabled(false);
1725 m_replyPrivate->setEnabled(false);
1726 m_replyAll->setEnabled(false);
1727 m_replyAllButMe->setEnabled(false);
1728 m_replyList->setEnabled(false);
1729 m_forwardAsAttachment->setEnabled(false);
1733 void MainWindow::slotUpdateMessageActions()
1735 Composer::RecipientList dummy;
1736 m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(),
1737 m_messageWidget->messageView->currentMessage(), dummy));
1738 m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(),
1739 m_messageWidget->messageView->currentMessage(), dummy));
1740 m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(),
1741 m_messageWidget->messageView->currentMessage(), dummy));
1742 m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(),
1743 m_messageWidget->messageView->currentMessage(), dummy));
1744 m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled()
1745 || m_replyAll->isEnabled() || m_replyList->isEnabled());
1747 // Check the default reply mode
1748 // I suspect this is not going to work for everybody. Suggestions welcome...
1749 if (m_replyList->isEnabled()) {
1750 m_replyButton->setDefaultAction(m_replyList);
1751 } else if (m_replyAllButMe->isEnabled()) {
1752 m_replyButton->setDefaultAction(m_replyAllButMe);
1753 } else {
1754 m_replyButton->setDefaultAction(m_replyPrivate);
1757 m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid());
1760 void MainWindow::scrollMessageUp()
1762 m_messageWidget->area->ensureVisible(0, 0, 0, 0);
1765 void MainWindow::slotReplyTo()
1767 m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE);
1770 void MainWindow::slotReplyAll()
1772 m_messageWidget->messageView->reply(this, Composer::REPLY_ALL);
1775 void MainWindow::slotReplyAllButMe()
1777 m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME);
1780 void MainWindow::slotReplyList()
1782 m_messageWidget->messageView->reply(this, Composer::REPLY_LIST);
1785 void MainWindow::slotReplyGuess()
1787 if (m_replyButton->defaultAction() == m_replyAllButMe) {
1788 slotReplyAllButMe();
1789 } else if (m_replyButton->defaultAction() == m_replyAll) {
1790 slotReplyAll();
1791 } else if (m_replyButton->defaultAction() == m_replyList) {
1792 slotReplyList();
1793 } else {
1794 slotReplyTo();
1798 void MainWindow::slotForwardAsAttachment()
1800 m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT);
1803 void MainWindow::slotComposeMailUrl(const QUrl &url)
1805 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this);
1808 void MainWindow::slotManageContact(const QUrl &url)
1810 Imap::Message::MailAddress addr;
1811 if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact")))
1812 return;
1814 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1815 if (!addressbook)
1816 return;
1818 addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name);
1821 void MainWindow::invokeContactEditor()
1823 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1824 if (!addressbook)
1825 return;
1827 addressbook->openAddressbookWindow();
1830 /** @short Create an MSAFactory as per the settings */
1831 MSA::MSAFactory *MainWindow::msaFactory()
1833 using namespace Common;
1834 QString method = m_settings->value(SettingsNames::msaMethodKey).toString();
1835 MSA::MSAFactory *msaFactory = 0;
1836 if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) {
1837 msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(),
1838 m_settings->value(SettingsNames::smtpPortKey).toInt(),
1839 (method == SettingsNames::methodSSMTP),
1840 (method == SettingsNames::methodSMTP)
1841 && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(),
1842 m_settings->value(SettingsNames::smtpAuthKey).toBool(),
1843 m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ?
1844 m_settings->value(SettingsNames::imapUserKey).toString() :
1845 m_settings->value(SettingsNames::smtpUserKey).toString());
1846 } else if (method == SettingsNames::methodSENDMAIL) {
1847 QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' '));
1848 if (args.isEmpty()) {
1849 return 0;
1851 QString appName = args.takeFirst();
1852 msaFactory = new MSA::SendmailFactory(appName, args);
1853 } else if (method == SettingsNames::methodImapSendmail) {
1854 if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) {
1855 return 0;
1857 msaFactory = new MSA::ImapSubmitFactory(qobject_cast<Imap::Mailbox::Model*>(imapAccess()->imapModel()));
1858 } else {
1859 return 0;
1861 return msaFactory;
1864 void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg)
1866 Gui::Util::messageBoxWarning(this, tr("Can't delete mailbox"),
1867 tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1870 void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg)
1872 Gui::Util::messageBoxWarning(this, tr("Can't create mailbox"),
1873 tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1876 void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg)
1878 Gui::Util::messageBoxWarning(this, tr("Can't open mailbox"),
1879 tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
1882 void MainWindow::slotMailboxChanged(const QModelIndex &mailbox)
1884 using namespace Imap::Mailbox;
1885 QString mailboxName = mailbox.data(RoleMailboxName).toString();
1886 bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() &&
1887 m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() &&
1888 mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString();
1889 QTreeView *tree = msgListWidget->tree;
1891 // Automatically trigger visibility of the TO and FROM columns
1892 if (isSentMailbox) {
1893 if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) {
1894 tree->hideColumn(MsgListModel::FROM);
1895 tree->showColumn(MsgListModel::TO);
1897 } else {
1898 if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) {
1899 tree->hideColumn(MsgListModel::TO);
1900 tree->showColumn(MsgListModel::FROM);
1904 updateMessageFlags();
1905 slotScrollToUnseenMessage();
1908 void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state)
1910 Q_UNUSED(parserId);
1911 static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE;
1912 QString message = connectionStateToString(state);
1914 if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) {
1915 // prevent excessive popups when we "reset the state" to something which is shown quite often
1916 showStatusMessage(QString());
1917 } else {
1918 showStatusMessage(message);
1920 previousState = state;
1923 void MainWindow::slotShowLinkTarget(const QString &link)
1925 if (link.isEmpty()) {
1926 QToolTip::hideText();
1927 } else {
1928 QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link)));
1932 void MainWindow::slotShowAboutTrojita()
1934 Ui::AboutDialog ui;
1935 QDialog *widget = new QDialog(this);
1936 widget->setAttribute(Qt::WA_DeleteOnClose);
1937 ui.setupUi(widget);
1938 ui.versionLabel->setText(Common::Application::version);
1939 ui.qtVersion->setText(QStringLiteral("<a href=\"about-qt\">Qt " QT_VERSION_STR "</a>"));
1940 connect(ui.qtVersion, &QLabel::linkActivated, qApp, &QApplication::aboutQt);
1942 std::vector<std::pair<QString, bool>> features;
1943 features.emplace_back(tr("Plugins"),
1944 #ifdef WITH_SHARED_PLUGINS
1945 true
1946 #else
1947 false
1948 #endif
1950 features.emplace_back(tr("Encrypted and signed messages"),
1951 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
1952 true
1953 #else
1954 false
1955 #endif
1957 features.emplace_back(tr("IMAP compression"),
1958 #ifdef TROJITA_HAVE_ZLIB
1959 true
1960 #else
1961 false
1962 #endif
1965 QString featuresText = QStringLiteral("<ul>");
1966 for (const auto x: features) {
1967 featuresText += x.second ?
1968 tr("<li>%1: supported</li>").arg(x.first)
1969 : tr("<li>%1: <strong>disabled</strong></li>").arg(x.first);
1971 featuresText += QStringLiteral("</ul>");
1972 ui.descriptionLabel->setText(ui.descriptionLabel->text() + featuresText);
1974 QStringList copyright;
1976 // Find the names of the authors and remove date codes from there
1977 QFile license(QStringLiteral(":/LICENSE"));
1978 license.open(QFile::ReadOnly);
1979 const QString prefix(QStringLiteral("Copyright (C) "));
1980 Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) {
1981 if (line.startsWith(prefix)) {
1982 const int pos = prefix.size();
1983 copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') +
1984 line.mid(pos).replace(QRegExp(QLatin1String("(\\d) - (\\d)")),
1985 QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2"));
1989 ui.credits->setTextFormat(Qt::PlainText);
1990 ui.credits->setText(copyright.join(QStringLiteral("\n")));
1991 widget->show();
1994 void MainWindow::slotDonateToTrojita()
1996 QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/"));
1999 void MainWindow::slotSaveCurrentMessageBody()
2001 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2002 Q_ASSERT(item.isValid());
2003 if (item.column() != 0)
2004 continue;
2005 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2006 continue;
2008 QModelIndex messageIndex = Imap::deproxifiedIndex(item);
2010 Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this);
2011 netAccess->setModelMessage(messageIndex);
2012 Imap::Network::FileDownloadManager *fileDownloadManager =
2013 new Imap::Network::FileDownloadManager(this, netAccess, messageIndex);
2014 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater);
2015 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater);
2016 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested,
2017 this, &MainWindow::slotDownloadMessageFileNameRequested);
2018 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError,
2019 this, &MainWindow::slotDownloadTransferError);
2020 connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater);
2021 fileDownloadManager->downloadMessage();
2025 void MainWindow::slotDownloadTransferError(const QString &errorString)
2027 Gui::Util::messageBoxCritical(this, tr("Can't save into file"),
2028 tr("Unable to save into file. Error:\n%1").arg(errorString));
2031 void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName)
2033 *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0,
2034 QFileDialog::HideNameFilterDetails);
2037 void MainWindow::slotViewMsgSource()
2039 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2040 Q_ASSERT(item.isValid());
2041 if (item.column() != 0)
2042 continue;
2043 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2044 continue;
2045 auto w = messageSourceWidget(item);
2046 //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox.
2047 w->setWindowTitle(tr("Message source of UID %1 in %2").arg(
2048 QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()),
2049 Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString()
2051 w->show();
2055 QWidget *MainWindow::messageSourceWidget(const QModelIndex &message)
2057 QModelIndex messageIndex = Imap::deproxifiedIndex(message);
2058 MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex);
2059 sourceWidget->setAttribute(Qt::WA_DeleteOnClose);
2060 QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget);
2061 sourceWidget->addAction(close);
2062 close->setShortcut(tr("Ctrl+W"));
2063 connect(close, &QAction::triggered, sourceWidget, &QWidget::close);
2064 return sourceWidget;
2067 void MainWindow::slotViewMsgHeaders()
2069 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2070 Q_ASSERT(item.isValid());
2071 if (item.column() != 0)
2072 continue;
2073 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2074 continue;
2075 QModelIndex messageIndex = Imap::deproxifiedIndex(item);
2077 auto widget = new MessageHeadersWidget(nullptr, messageIndex);
2078 widget->setAttribute(Qt::WA_DeleteOnClose);
2079 QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget);
2080 widget->addAction(close);
2081 close->setShortcut(tr("Ctrl+W"));
2082 connect(close, &QAction::triggered, widget, &QWidget::close);
2083 widget->show();
2087 #ifdef XTUPLE_CONNECT
2088 void MainWindow::slotXtSyncCurrentMailbox()
2090 QModelIndex index = mboxTree->currentIndex();
2091 if (! index.isValid())
2092 return;
2094 QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2095 QSettings s;
2096 QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList();
2097 if (xtIncludeMailboxInSync->isChecked()) {
2098 if (! mailboxes.contains(mailbox)) {
2099 mailboxes.append(mailbox);
2101 } else {
2102 mailboxes.removeAll(mailbox);
2104 s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2105 QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2106 prettyMboxModel->xtConnectStatusChanged(index);
2108 #endif
2110 void MainWindow::slotSubscribeCurrentMailbox()
2112 QModelIndex index = mboxTree->currentIndex();
2113 if (! index.isValid())
2114 return;
2116 QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2117 if (m_actionSubscribeMailbox->isChecked()) {
2118 imapModel()->subscribeMailbox(mailbox);
2119 } else {
2120 imapModel()->unsubscribeMailbox(mailbox);
2124 void MainWindow::slotShowOnlySubscribed()
2126 if (m_actionShowOnlySubscribed->isEnabled()) {
2127 m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked());
2128 prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked());
2132 void MainWindow::slotScrollToUnseenMessage()
2134 // Now this is much, much more reliable than messing around with finding out an "interesting message"...
2135 if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) {
2136 // we're using some funky sorting, better don't scroll anywhere
2138 if (m_actionSortDescending->isChecked()) {
2139 msgListWidget->tree->scrollToTop();
2140 } else {
2141 msgListWidget->tree->scrollToBottom();
2145 void MainWindow::slotScrollToCurrent()
2147 // TODO: begs for lambda
2148 if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) {
2149 vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0
2153 void MainWindow::slotThreadMsgList()
2155 // We want to save user's preferences and not override them with "threading disabled" when the server
2156 // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here.
2157 const bool useThreading = actionThreadMsgList->isChecked();
2159 // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make
2160 // sorting rather seldomly used as people shall instead use proper threading.
2161 if (useThreading) {
2162 m_actionSortThreading->setEnabled(true);
2163 if (!m_actionSortThreading->isChecked())
2164 m_actionSortThreading->trigger();
2165 m_actionSortNone->setEnabled(false);
2166 } else {
2167 m_actionSortNone->setEnabled(true);
2168 if (!m_actionSortNone->isChecked())
2169 m_actionSortNone->trigger();
2170 m_actionSortThreading->setEnabled(false);
2173 QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex();
2175 if (useThreading && actionThreadMsgList->isEnabled()) {
2176 msgListWidget->tree->setRootIsDecorated(true);
2177 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true);
2178 } else {
2179 msgListWidget->tree->setRootIsDecorated(false);
2180 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false);
2182 m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading));
2184 if (currentItem.isValid()) {
2185 msgListWidget->tree->scrollTo(currentItem);
2186 } else {
2187 // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view
2188 // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same"
2189 // objects as what was visible before the reshuffling.
2190 slotScrollToUnseenMessage();
2194 void MainWindow::slotSortingPreferenceChanged()
2196 Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
2198 using namespace Imap::Mailbox;
2200 int column = -1;
2201 if (m_actionSortByArrival->isChecked()) {
2202 column = MsgListModel::RECEIVED_DATE;
2203 } else if (m_actionSortByCc->isChecked()) {
2204 column = MsgListModel::CC;
2205 } else if (m_actionSortByDate->isChecked()) {
2206 column = MsgListModel::DATE;
2207 } else if (m_actionSortByFrom->isChecked()) {
2208 column = MsgListModel::FROM;
2209 } else if (m_actionSortBySize->isChecked()) {
2210 column = MsgListModel::SIZE;
2211 } else if (m_actionSortBySubject->isChecked()) {
2212 column = MsgListModel::SUBJECT;
2213 } else if (m_actionSortByTo->isChecked()) {
2214 column = MsgListModel::TO;
2215 } else {
2216 column = -1;
2219 msgListWidget->tree->header()->setSortIndicator(column, order);
2222 void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order)
2224 // don't do anything during initialization
2225 if (!m_actionSortNone)
2226 return;
2228 using namespace Imap::Mailbox;
2229 QAction *action;
2231 switch (column) {
2232 case MsgListModel::SEEN:
2233 case MsgListModel::FLAGGED:
2234 case MsgListModel::ATTACHMENT:
2235 case MsgListModel::COLUMN_COUNT:
2236 case MsgListModel::BCC:
2237 case -1:
2238 if (actionThreadMsgList->isChecked())
2239 action = m_actionSortThreading;
2240 else
2241 action = m_actionSortNone;
2242 break;
2243 case MsgListModel::SUBJECT:
2244 action = m_actionSortBySubject;
2245 break;
2246 case MsgListModel::FROM:
2247 action = m_actionSortByFrom;
2248 break;
2249 case MsgListModel::TO:
2250 action = m_actionSortByTo;
2251 break;
2252 case MsgListModel::CC:
2253 action = m_actionSortByCc;
2254 break;
2255 case MsgListModel::DATE:
2256 action = m_actionSortByDate;
2257 break;
2258 case MsgListModel::RECEIVED_DATE:
2259 action = m_actionSortByArrival;
2260 break;
2261 case MsgListModel::SIZE:
2262 action = m_actionSortBySize;
2263 break;
2264 default:
2265 action = m_actionSortNone;
2268 action->setChecked(true);
2269 if (order == Qt::DescendingOrder)
2270 m_actionSortDescending->setChecked(true);
2271 else
2272 m_actionSortAscending->setChecked(true);
2275 void MainWindow::slotSearchRequested(const QStringList &searchConditions)
2277 if (!searchConditions.isEmpty() && actionThreadMsgList->isChecked()) {
2278 // right now, searching and threading doesn't play well together at all
2279 actionThreadMsgList->trigger();
2281 Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel =
2282 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel());
2283 threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(),
2284 threadingMsgListModel->currentSortOrder());
2287 void MainWindow::slotHideRead()
2289 const bool hideRead = actionHideRead->isChecked();
2290 prettyMsgListModel->setHideRead(hideRead);
2291 m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead));
2294 void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities)
2296 msgListWidget->tree->header()->viewport()->removeEventFilter(this);
2297 if (capabilities.contains(QStringLiteral("SORT"))) {
2298 m_actionSortByDate->actionGroup()->setEnabled(true);
2299 } else {
2300 m_actionSortByDate->actionGroup()->setEnabled(false);
2301 msgListWidget->tree->header()->viewport()->installEventFilter(this);
2304 msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY")));
2306 m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED")));
2307 m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() &&
2308 m_settings->value(
2309 Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool());
2310 m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled());
2312 const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities();
2313 Q_FOREACH(const QString &capability, capabilities) {
2314 if (supportedCapabilities.contains(capability)) {
2315 actionThreadMsgList->setEnabled(true);
2316 if (actionThreadMsgList->isChecked())
2317 slotThreadMsgList();
2318 return;
2321 actionThreadMsgList->setEnabled(false);
2324 void MainWindow::slotShowImapInfo()
2326 QString caps;
2327 Q_FOREACH(const QString &cap, imapModel()->capabilities()) {
2328 caps += tr("<li>%1</li>\n").arg(cap);
2331 QString idString;
2332 if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) {
2333 QMap<QByteArray,QByteArray> serverId = imapModel()->serverId();
2335 #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \
2336 QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown");
2337 IMAP_ID_FIELD(serverName, "name");
2338 IMAP_ID_FIELD(serverVersion, "version");
2339 IMAP_ID_FIELD(os, "os");
2340 IMAP_ID_FIELD(osVersion, "os-version");
2341 IMAP_ID_FIELD(vendor, "vendor");
2342 IMAP_ID_FIELD(supportUrl, "support-url");
2343 IMAP_ID_FIELD(address, "address");
2344 IMAP_ID_FIELD(date, "date");
2345 IMAP_ID_FIELD(command, "command");
2346 IMAP_ID_FIELD(arguments, "arguments");
2347 IMAP_ID_FIELD(environment, "environment");
2348 #undef IMAP_ID_FIELD
2349 if (has_serverName) {
2350 idString = tr("<p>");
2351 if (has_serverVersion)
2352 idString += tr("Server: %1 %2").arg(serverName, serverVersion);
2353 else
2354 idString += tr("Server: %1").arg(serverName);
2356 if (has_vendor) {
2357 idString += tr(" (%1)").arg(vendor);
2359 if (has_os) {
2360 if (has_osVersion)
2361 idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion);
2362 else
2363 idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os);
2365 idString += tr("</p>");
2366 } else {
2367 idString = tr("<p>The IMAP server did not return usable information about itself.</p>");
2369 QString fullId;
2370 for (QMap<QByteArray,QByteArray>::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) {
2371 fullId += tr("<li>%1: %2</li>").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped());
2373 idString += tr("<ul>%1</ul>").arg(fullId);
2374 } else {
2375 idString = tr("<p>The server has not provided information about its software version.</p>");
2378 QMessageBox::information(this, tr("IMAP Server Information"),
2379 tr("%1"
2380 "<p>The following capabilities are currently advertised:</p>\n"
2381 "<ul>\n%2</ul>").arg(idString, caps));
2384 QSize MainWindow::sizeHint() const
2386 return QSize(1150, 980);
2389 void MainWindow::slotUpdateWindowTitle()
2391 QModelIndex mailbox = qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox();
2392 QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
2393 if (!profileName.isEmpty())
2394 profileName = QLatin1String(" [") + profileName + QLatin1Char(']');
2395 if (mailbox.isValid()) {
2396 if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) {
2397 setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt())
2398 .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2399 } else {
2400 setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2402 } else {
2403 setWindowTitle(trUtf8("Trojitá") + profileName);
2407 void MainWindow::slotLayoutCompact()
2409 saveSizesAndState();
2410 if (!m_mainHSplitter) {
2411 m_mainHSplitter = new QSplitter();
2412 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2413 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2415 if (!m_mainVSplitter) {
2416 m_mainVSplitter = new QSplitter();
2417 m_mainVSplitter->setOrientation(Qt::Vertical);
2418 connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2419 connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2422 m_mainVSplitter->addWidget(msgListWidget);
2423 m_mainVSplitter->addWidget(m_messageWidget);
2424 m_mainHSplitter->addWidget(mboxTree);
2425 m_mainHSplitter->addWidget(m_mainVSplitter);
2427 mboxTree->show();
2428 msgListWidget->show();
2429 m_messageWidget->show();
2430 m_mainVSplitter->show();
2431 m_mainHSplitter->show();
2433 // The mboxTree shall not expand...
2434 m_mainHSplitter->setStretchFactor(0, 0);
2435 // ...while the msgListTree shall consume all the remaining space
2436 m_mainHSplitter->setStretchFactor(1, 1);
2437 // The CompleteMessageWidget shall not not collapse
2438 m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false);
2440 setCentralWidget(m_mainHSplitter);
2442 delete m_mainStack;
2444 m_layoutMode = LAYOUT_COMPACT;
2445 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact);
2446 applySizesAndState();
2449 void MainWindow::slotLayoutWide()
2451 saveSizesAndState();
2452 if (!m_mainHSplitter) {
2453 m_mainHSplitter = new QSplitter();
2454 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2455 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2458 m_mainHSplitter->addWidget(mboxTree);
2459 m_mainHSplitter->addWidget(msgListWidget);
2460 m_mainHSplitter->addWidget(m_messageWidget);
2461 msgListWidget->resize(mboxTree->size());
2462 m_messageWidget->resize(mboxTree->size());
2463 m_mainHSplitter->setStretchFactor(0, 0);
2464 m_mainHSplitter->setStretchFactor(1, 1);
2465 m_mainHSplitter->setStretchFactor(2, 1);
2467 m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false);
2469 mboxTree->show();
2470 msgListWidget->show();
2471 m_messageWidget->show();
2472 m_mainHSplitter->show();
2474 setCentralWidget(m_mainHSplitter);
2476 delete m_mainStack;
2477 delete m_mainVSplitter;
2479 m_layoutMode = LAYOUT_WIDE;
2480 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide);
2481 applySizesAndState();
2484 void MainWindow::slotLayoutOneAtTime()
2486 saveSizesAndState();
2487 if (m_mainStack)
2488 return;
2490 m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack);
2491 setCentralWidget(m_mainStack);
2493 delete m_mainHSplitter;
2494 delete m_mainVSplitter;
2496 m_layoutMode = LAYOUT_ONE_AT_TIME;
2497 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime);
2498 applySizesAndState();
2501 Imap::Mailbox::Model *MainWindow::imapModel() const
2503 return qobject_cast<Imap::Mailbox::Model *>(m_imapAccess->imapModel());
2506 void MainWindow::desktopGeometryChanged()
2508 m_delayedStateSaving->start();
2511 QString MainWindow::settingsKeyForLayout(const LayoutMode layout)
2513 switch (layout) {
2514 case LAYOUT_COMPACT:
2515 return Common::SettingsNames::guiSizesInMainWinWhenCompact;
2516 case LAYOUT_WIDE:
2517 return Common::SettingsNames::guiSizesInMainWinWhenWide;
2518 case LAYOUT_ONE_AT_TIME:
2519 return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime;
2520 break;
2522 return QString();
2525 void MainWindow::saveSizesAndState()
2527 if (m_skipSavingOfUI)
2528 return;
2530 QRect geometry = qApp->desktop()->availableGeometry(this);
2531 QString key = settingsKeyForLayout(m_layoutMode);
2532 if (key.isEmpty())
2533 return;
2535 QList<QByteArray> items;
2536 items << saveGeometry();
2537 items << saveState();
2538 items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray());
2539 items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray());
2540 items << msgListWidget->tree->header()->saveState();
2541 items << QByteArray::number(msgListWidget->tree->header()->count());
2542 for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) {
2543 items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i));
2545 // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number
2546 items << QByteArray::number(menuBar()->isVisible());
2547 QByteArray buf;
2548 QDataStream stream(&buf, QIODevice::WriteOnly);
2549 stream << items.size();
2550 Q_FOREACH(const QByteArray &item, items) {
2551 stream << item;
2554 m_settings->setValue(key.arg(QString::number(geometry.width())), buf);
2557 void MainWindow::saveRawStateSetting(bool enabled)
2559 m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled);
2562 void MainWindow::applySizesAndState()
2564 QRect geometry = qApp->desktop()->availableGeometry(this);
2565 QString key = settingsKeyForLayout(m_layoutMode);
2566 if (key.isEmpty())
2567 return;
2569 QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray();
2570 if (buf.isEmpty())
2571 return;
2573 int size;
2574 QDataStream stream(&buf, QIODevice::ReadOnly);
2575 stream >> size;
2576 QByteArray item;
2578 // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to
2579 // save our geometries and state, which is what we must avoid while this method is executing.
2580 bool skipSaving = m_skipSavingOfUI;
2581 m_skipSavingOfUI = true;
2583 if (size-- && !stream.atEnd()) {
2584 stream >> item;
2586 // https://bugreports.qt-project.org/browse/QTBUG-30636
2587 if (windowState() & Qt::WindowMaximized) {
2588 // restoreGeometry(.) restores the wrong size for at least maximized window
2589 // However, QWidget does also not notice that the configure request for this
2590 // is ignored by many window managers (because users really don't like when windows
2591 // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from
2592 // the wrong assumption the request would have been hononred.
2593 // So we just "fix" the internal geometry immediately afterwards to prevent
2594 // mislayouting
2595 // There's atm. no flicker due to this (and because Qt compresses events)
2596 // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true)
2597 QRect oldGeometry = MainWindow::geometry();
2598 restoreGeometry(item);
2599 if (windowState() & Qt::WindowMaximized)
2600 setGeometry(oldGeometry);
2601 } else {
2602 restoreGeometry(item);
2603 if (windowState() & Qt::WindowMaximized) {
2604 // ensure to try setting the proper geometry and have the WM constrain us
2605 setGeometry(QApplication::desktop()->availableGeometry());
2610 if (size-- && !stream.atEnd()) {
2611 stream >> item;
2612 restoreState(item);
2615 if (size-- && !stream.atEnd()) {
2616 stream >> item;
2617 if (m_mainVSplitter) {
2618 m_mainVSplitter->restoreState(item);
2622 if (size-- && !stream.atEnd()) {
2623 stream >> item;
2624 if (m_mainHSplitter) {
2625 m_mainHSplitter->restoreState(item);
2629 if (size-- && !stream.atEnd()) {
2630 stream >> item;
2631 msgListWidget->tree->header()->restoreState(item);
2632 // got to manually update the state of the actions which control the visibility state
2633 msgListWidget->tree->updateActionsAfterRestoredState();
2636 connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns);
2638 if (size-- && !stream.atEnd()) {
2639 stream >> item;
2640 bool ok;
2641 int columns = item.toInt(&ok);
2642 if (ok) {
2643 msgListWidget->tree->header()->setStretchLastSection(false);
2644 for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) {
2645 stream >> item;
2646 int sectionSize = item.toInt();
2647 QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i);
2648 if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) {
2649 // fun fact: user cannot resize by mouse when size <= 0
2650 msgListWidget->tree->setColumnWidth(i, sectionSize);
2651 } else {
2652 msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i));
2654 msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode);
2659 if (size-- && !stream.atEnd()) {
2660 stream >> item;
2661 bool ok;
2662 bool visibility = item.toInt(&ok);
2663 if (ok) {
2664 menuBar()->setVisible(visibility);
2665 showMenuBar->setChecked(visibility);
2669 m_skipSavingOfUI = skipSaving;
2672 void MainWindow::resizeEvent(QResizeEvent *)
2674 m_delayedStateSaving->start();
2677 /** @short Make sure that the message gets loaded after the splitters have changed their position */
2678 void MainWindow::possiblyLoadMessageOnSplittersChanged()
2680 if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) {
2681 // We do not have to check whether it's a different message; the setMessage() will do this or us
2682 // and there are multiple proxy models involved anyway
2683 QModelIndex index = msgListWidget->tree->currentIndex();
2684 if (index.isValid()) {
2685 // OTOH, setting an invalid QModelIndex would happily assert-fail
2686 m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex());
2691 Imap::ImapAccess *MainWindow::imapAccess() const
2693 return m_imapAccess;
2696 void MainWindow::enableLoggingToDisk()
2698 imapLogger->slotSetPersistentLogging(true);
2701 void MainWindow::slotPluginsChanged()
2703 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
2704 if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow))
2705 m_actionContactEditor->setEnabled(false);
2706 else
2707 m_actionContactEditor->setEnabled(true);
2710 /** @short Update the default action to make sure that we show a correct status of the network connection */
2711 void MainWindow::updateNetworkIndication()
2713 if (QAction *action = qobject_cast<QAction*>(sender())) {
2714 if (action->isChecked()) {
2715 m_netToolbarDefaultAction->setIcon(action->icon());
2720 void MainWindow::showStatusMessage(const QString &message)
2722 networkIndicator->setToolTip(message);
2723 if (isActiveWindow())
2724 QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message);
2727 void MainWindow::slotMessageModelChanged(QAbstractItemModel *model)
2729 mailMimeTree->setModel(model);