Add credits for Marek
[trojita.git] / src / Gui / Window.cpp
blob8d74874f81ea1efaa6126e68f977d6bc7ce6dd17
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 <QPainterPath>
36 #include <QProgressBar>
37 #include <QRegularExpression>
38 #include <QScopedPointer>
39 #include <QScrollBar>
40 #include <QSplitter>
41 #include <QSslError>
42 #include <QSslKey>
43 #include <QStackedWidget>
44 #include <QStatusBar>
45 #include <QTextDocument>
46 #include <QToolBar>
47 #include <QToolButton>
48 #include <QToolTip>
49 #include <QUrl>
50 #include <QWheelEvent>
52 #include "configure.cmake.h"
53 #include "Common/Application.h"
54 #include "Common/InvokeMethod.h"
55 #include "Common/Paths.h"
56 #include "Common/PortNumbers.h"
57 #include "Common/SettingsNames.h"
58 #include "Composer/Mailto.h"
59 #include "Composer/SenderIdentitiesModel.h"
60 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
61 # ifdef TROJITA_HAVE_GPGMEPP
62 # include "Cryptography/GpgMe++.h"
63 # endif
64 #endif
65 #include "Imap/Model/ImapAccess.h"
66 #include "Imap/Model/MailboxTree.h"
67 #include "Imap/Model/Model.h"
68 #include "Imap/Model/ModelWatcher.h"
69 #include "Imap/Model/MsgListModel.h"
70 #include "Imap/Model/NetworkWatcher.h"
71 #include "Imap/Model/PrettyMailboxModel.h"
72 #include "Imap/Model/PrettyMsgListModel.h"
73 #include "Imap/Model/SpecialFlagNames.h"
74 #include "Imap/Model/ThreadingMsgListModel.h"
75 #include "Imap/Model/FavoriteTagsModel.h"
76 #include "Imap/Model/Utils.h"
77 #include "Imap/Tasks/ImapTask.h"
78 #include "Imap/Network/FileDownloadManager.h"
79 #include "MSA/ImapSubmit.h"
80 #include "MSA/Sendmail.h"
81 #include "MSA/SMTP.h"
82 #include "Plugins/AddressbookPlugin.h"
83 #include "Plugins/PasswordPlugin.h"
84 #include "Plugins/PluginManager.h"
85 #include "CompleteMessageWidget.h"
86 #include "ComposeWidget.h"
87 #include "MailBoxTreeView.h"
88 #include "MessageListWidget.h"
89 #include "MessageView.h"
90 #include "MessageSourceWidget.h"
91 #include "Gui/MessageHeadersWidget.h"
92 #include "MsgListView.h"
93 #include "OnePanelAtTimeWidget.h"
94 #include "PasswordDialog.h"
95 #include "ProtocolLoggerWidget.h"
96 #include "SettingsDialog.h"
97 #include "SimplePartWidget.h"
98 #include "Streams/SocketFactory.h"
99 #include "TaskProgressIndicator.h"
100 #include "Util.h"
101 #include "Window.h"
102 #include "ShortcutHandler/ShortcutHandler.h"
104 #include "ui_CreateMailboxDialog.h"
105 #include "ui_AboutDialog.h"
106 #include "ui_ImapInfoDialog.h"
108 #include "Imap/Model/ModelTest/modeltest.h"
109 #include "UiUtils/IconLoader.h"
110 #include "UiUtils/QaimDfsIterator.h"
112 /** @short All user-facing widgets and related classes */
113 namespace Gui
116 static const char * const netErrorUnseen = "net_error_unseen";
118 MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0),
119 m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0),
120 m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0)
122 setAttribute(Qt::WA_AlwaysShowToolTips);
123 // m_pluginManager must be created before calling createWidgets
124 m_pluginManager = new Plugins::PluginManager(this, m_settings,
125 Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin,
126 Common::SettingsNames::spellcheckerPlugin);
127 connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged);
128 connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) {
129 Gui::Util::messageBoxWarning(this, tr("Plugin Error"),
130 //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption.
131 tr("A plugin failed to load, therefore some functionality might be lost. "
132 "You might want to update your system or report a bug to your vendor."
133 "\n\n%1").arg(errorMessage));
135 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
136 Plugins::PluginManager::MimePartReplacers replacers;
137 #ifdef TROJITA_HAVE_GPGMEPP
138 replacers.emplace_back(std::make_shared<Cryptography::GpgMeReplacer>());
139 #endif
140 m_pluginManager->setMimePartReplacers(replacers);
141 #endif
143 // ImapAccess contains a wrapper for retrieving passwords through some plugin.
144 // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class,
145 // which means that ImapAccess has to be constructed before we go and open the settings dialog.
147 // FIXME: use another account-id at some point in future
148 // we are now using the profile to avoid overwriting passwords of
149 // other profiles in secure storage
150 QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
151 m_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName);
152 connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError);
153 connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection);
155 ShortcutHandler *shortcutHandler = new ShortcutHandler(this);
156 shortcutHandler->setSettingsObject(m_settings);
157 defineActions();
158 shortcutHandler->readSettings(); // must happen after defineActions()
160 // must be created before calling createWidgets
161 m_favoriteTags = new Imap::Mailbox::FavoriteTagsModel(this);
162 m_favoriteTags->loadFromSettings(*m_settings);
164 createWidgets();
166 Imap::migrateSettings(m_settings);
168 m_senderIdentities = new Composer::SenderIdentitiesModel(this);
169 m_senderIdentities->loadFromSettings(*m_settings);
171 if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) {
172 QTimer::singleShot(0, this, SLOT(slotShowSettings()));
176 setupModels();
177 createActions();
178 createMenus();
179 slotToggleSysTray();
180 slotPluginsChanged();
182 slotFavoriteTagsChanged();
183 connect(m_favoriteTags, &QAbstractItemModel::modelReset, this, &MainWindow::slotFavoriteTagsChanged);
184 connect(m_favoriteTags, &QAbstractItemModel::layoutChanged, this, &MainWindow::slotFavoriteTagsChanged);
185 connect(m_favoriteTags, &QAbstractItemModel::rowsMoved, this, &MainWindow::slotFavoriteTagsChanged);
186 connect(m_favoriteTags, &QAbstractItemModel::rowsInserted, this, &MainWindow::slotFavoriteTagsChanged);
187 connect(m_favoriteTags, &QAbstractItemModel::rowsRemoved, this, &MainWindow::slotFavoriteTagsChanged);
188 connect(m_favoriteTags, &QAbstractItemModel::dataChanged, this, &MainWindow::slotFavoriteTagsChanged);
190 // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro
191 QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl");
192 QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact");
194 slotUpdateWindowTitle();
196 CALL_LATER_NOARG(this, recoverDrafts);
198 if (m_actionLayoutWide->isEnabled() &&
199 m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) {
200 m_actionLayoutWide->trigger();
201 } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) {
202 m_actionLayoutOneAtTime->trigger();
203 } else {
204 m_actionLayoutCompact->trigger();
207 connect(qApp, &QGuiApplication::applicationStateChanged, this,
208 [&](Qt::ApplicationState state) {
209 if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) {
210 m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
211 m_networkErrorMessageBox->show();
215 // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but
216 // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time.
217 // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence
218 // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts
219 // to that and only after the panel gets resized, the available size of "the rest" is correct again).
220 // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially
221 // because every change bumps the timer anyway, as Thomas pointed out).
222 QTimer *delayedResize = new QTimer(this);
223 delayedResize->setSingleShot(true);
224 delayedResize->setInterval(500);
225 connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged);
226 connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast<void (QTimer::*)()>(&QTimer::start));
227 m_skipSavingOfUI = false;
230 void MainWindow::defineActions()
232 ShortcutHandler *shortcutHandler = ShortcutHandler::instance();
233 shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit);
234 shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New);
235 shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft..."));
236 shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M"));
237 shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E"));
238 shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M"));
239 shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N"));
240 shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P"));
241 shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString());
242 shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S"));
243 shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J"));
244 shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J"));
245 shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message..."));
246 shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source..."));
247 shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U"));
248 shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A"));
249 shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R"));
250 shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R"));
251 shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L"));
252 shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R"));
253 shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F"));
254 shortcutHandler->defineAction(QStringLiteral("action_resend"), QStringLiteral("mail-resend"), tr("Resend..."));
255 shortcutHandler->defineAction(QStringLiteral("action_archive"), QStringLiteral("mail-move-to-archive"), tr("&Archive"), QStringLiteral("A"));
256 shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book..."));
257 shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline"));
258 shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection"));
259 shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access"));
260 shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window"));
261 shortcutHandler->defineAction(QStringLiteral("action_open_messagewindow"), QString(), tr("Open message in New Window..."), QStringLiteral("Ctrl+Return"));
262 shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString());
263 shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn);
264 shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut);
265 shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size"));
266 shortcutHandler->defineAction(QStringLiteral("action_focus_mailbox_tree"), QString(), tr("Move Focus to Mailbox List"));
267 shortcutHandler->defineAction(QStringLiteral("action_focus_msg_list"), QString(), tr("Move Focus to Message List"));
268 shortcutHandler->defineAction(QStringLiteral("action_focus_quick_search"), QString(), tr("Move Focus to Quick Search"), QStringLiteral("/"));
269 shortcutHandler->defineAction(QStringLiteral("action_tag_1"), QStringLiteral("mail-tag-1"), tr("Tag with &1st tag"), QStringLiteral("1"));
270 shortcutHandler->defineAction(QStringLiteral("action_tag_2"), QStringLiteral("mail-tag-2"), tr("Tag with &2nd tag"), QStringLiteral("2"));
271 shortcutHandler->defineAction(QStringLiteral("action_tag_3"), QStringLiteral("mail-tag-3"), tr("Tag with &3rd tag"), QStringLiteral("3"));
272 shortcutHandler->defineAction(QStringLiteral("action_tag_4"), QStringLiteral("mail-tag-4"), tr("Tag with &4th tag"), QStringLiteral("4"));
273 shortcutHandler->defineAction(QStringLiteral("action_tag_5"), QStringLiteral("mail-tag-5"), tr("Tag with &5th tag"), QStringLiteral("5"));
274 shortcutHandler->defineAction(QStringLiteral("action_tag_6"), QStringLiteral("mail-tag-6"), tr("Tag with &6th tag"), QStringLiteral("6"));
275 shortcutHandler->defineAction(QStringLiteral("action_tag_7"), QStringLiteral("mail-tag-7"), tr("Tag with &7th tag"), QStringLiteral("7"));
276 shortcutHandler->defineAction(QStringLiteral("action_tag_8"), QStringLiteral("mail-tag-8"), tr("Tag with &8th tag"), QStringLiteral("8"));
277 shortcutHandler->defineAction(QStringLiteral("action_tag_9"), QStringLiteral("mail-tag-9"), tr("Tag with &9th tag"), QStringLiteral("9"));
280 void MainWindow::createActions()
282 // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default:
284 // Thunderbird:
285 // private: Ctrl+R
286 // all: Ctrl+Shift+R
287 // list: Ctrl+Shift+L
288 // forward: Ctrl+L
289 // (no shortcuts for type of forwarding)
290 // resend: ctrl+B
291 // new message: Ctrl+N
293 // KMail:
294 // "reply": R
295 // private: Shift+A
296 // all: A
297 // list: L
298 // forward as attachment: F
299 // forward inline: Shift+F
300 // resend: E
301 // new: Ctrl+N
303 m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this);
305 m_mainToolbar = addToolBar(tr("Navigation"));
306 m_mainToolbar->setObjectName(QStringLiteral("mainToolbar"));
308 reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this);
309 connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList);
311 resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this);
312 connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox);
314 reloadAllMailboxes = new QAction(tr("&Reload Everything"), this);
315 // connect later
317 exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this);
318 exitAction->setStatusTip(tr("Exit the application"));
320 netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline"));
321 netOffline->setCheckable(true);
322 // connect later
323 netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive"));
324 netExpensive->setCheckable(true);
325 // connect later
326 netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online"));
327 netOnline->setCheckable(true);
328 // connect later
330 QActionGroup *netPolicyGroup = new QActionGroup(this);
331 netPolicyGroup->setExclusive(true);
332 netPolicyGroup->addAction(netOffline);
333 netPolicyGroup->addAction(netExpensive);
334 netPolicyGroup->addAction(netOnline);
336 //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts
337 showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this);
338 showFullView->setCheckable(true);
339 connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible);
340 connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked);
342 //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox
343 showTaskView = new QAction(tr("Show ImapTask t&ree"), this);
344 showTaskView->setCheckable(true);
345 connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible);
346 connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked);
348 //: a debugging tool showing the mime tree of the current message
349 showMimeView = new QAction(tr("Show &MIME tree"), this);
350 showMimeView->setCheckable(true);
351 connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible);
352 connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked);
354 showProtocolLogger = new QAction(tr("Show protocol &log"), this);
355 showProtocolLogger->setCheckable(true);
356 connect(showProtocolLogger, &QAction::toggled, protocolLoggerDock, &QWidget::setVisible);
357 connect(protocolLoggerDock, &QDockWidget::visibilityChanged, showProtocolLogger, &QAction::setChecked);
359 //: file to save the debug log into
360 logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this);
361 logPersistent->setCheckable(true);
362 connect(logPersistent, &QAction::triggered, protocolLogger, &ProtocolLoggerWidget::slotSetPersistentLogging);
363 connect(protocolLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked);
365 showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this);
366 connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo);
368 showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this);
369 showMenuBar->setCheckable(true);
370 showMenuBar->setChecked(true);
371 connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible);
372 connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
374 showToolBar = new QAction(tr("Show &Toolbar"), this);
375 showToolBar->setCheckable(true);
376 connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible);
377 connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked);
378 connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
380 configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")), tr("&Settings..."), this);
381 connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings);
383 QAction *triggerSearch = new QAction(this);
384 addAction(triggerSearch);
385 triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, =")));
386 connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch);
388 addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_quick_search"),
389 msgListWidget, SLOT(focusSearch()), this));
391 addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_mailbox_tree"), mboxTree,
392 SLOT(setFocus()), this));
393 addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_msg_list"), msgListWidget->tree,
394 SLOT(setFocus()), this));
396 m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this);
397 m_oneAtTimeGoBack->setEnabled(false);
399 composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this);
400 m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this);
402 expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this);
404 m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this);
405 m_resend = ShortcutHandler::instance()->createAction(QStringLiteral("action_resend"), this, SLOT(slotResend()), this);
406 markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this);
407 markAsRead->setCheckable(true);
408 msgListWidget->tree->addAction(markAsRead);
409 connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead);
411 m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this);
412 msgListWidget->tree->addAction(m_nextMessage);
413 m_messageWidget->messageView->addAction(m_nextMessage);
415 m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this);
416 msgListWidget->tree->addAction(m_previousMessage);
417 m_messageWidget->messageView->addAction(m_previousMessage);
419 markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this);
420 markAsDeleted->setCheckable(true);
421 msgListWidget->tree->addAction(markAsDeleted);
422 connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted);
424 markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this);
425 markAsFlagged->setCheckable(true);
426 msgListWidget->tree->addAction(markAsFlagged);
427 connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged);
429 markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this);
430 markAsJunk->setCheckable(true);
431 msgListWidget->tree->addAction(markAsJunk);
432 connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk);
434 markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this);
435 markAsNotJunk->setCheckable(true);
436 msgListWidget->tree->addAction(markAsNotJunk);
437 connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk);
439 saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this);
440 msgListWidget->tree->addAction(saveWholeMessage);
442 viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this);
443 msgListWidget->tree->addAction(viewMsgSource);
445 viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this);
446 msgListWidget->tree->addAction(viewMsgHeaders);
448 msgListWidget->tree->addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_open_messagewindow"), this,
449 SLOT(openCompleteMessageWidget()), this));
451 moveToArchive = ShortcutHandler::instance()->createAction(QStringLiteral("action_archive"), this);
452 msgListWidget->tree->addAction(moveToArchive);
453 connect(moveToArchive, &QAction::triggered, this, &MainWindow::handleMoveToArchive);
455 auto addTagAction = [=](int row) {
456 QAction *tag = ShortcutHandler::instance()->createAction(QStringLiteral("action_tag_").append(QString::number(row)), this);
457 tag->setCheckable(true);
458 msgListWidget->tree->addAction(tag);
459 connect(tag, &QAction::triggered, this, [=](const bool checked) {
460 handleTag(checked, row - 1);
462 return tag;
464 tag1 = addTagAction(1);
465 tag2 = addTagAction(2);
466 tag3 = addTagAction(3);
467 tag4 = addTagAction(4);
468 tag5 = addTagAction(5);
469 tag6 = addTagAction(6);
470 tag7 = addTagAction(7);
471 tag8 = addTagAction(8);
472 tag9 = addTagAction(9);
474 //: "mailbox" as a "folder of messages", not as a "mail account"
475 createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this);
476 connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent);
478 //: "mailbox" as a "folder of messages", not as a "mail account"
479 createTopMailbox = new QAction(tr("Create &New Mailbox..."), this);
480 connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox);
482 m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this);
483 connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead);
485 //: "mailbox" as a "folder of messages", not as a "mail account"
486 deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this);
487 connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox);
489 #ifdef XTUPLE_CONNECT
490 xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this);
491 xtIncludeMailboxInSync->setCheckable(true);
492 connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox()));
493 #endif
495 m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this);
496 m_replyPrivate->setEnabled(false);
498 m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this);
499 m_replyAllButMe->setEnabled(false);
501 m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this);
502 m_replyAll->setEnabled(false);
504 m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this);
505 m_replyList->setEnabled(false);
507 m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this);
508 m_replyGuess->setEnabled(true);
510 actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this);
511 actionThreadMsgList->setCheckable(true);
512 // This action is enabled/disabled by model's capabilities
513 actionThreadMsgList->setEnabled(false);
514 if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) {
515 actionThreadMsgList->setChecked(true);
516 // The actual threading will be performed only when model updates its capabilities
518 connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList);
520 QActionGroup *sortOrderGroup = new QActionGroup(this);
521 m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup);
522 m_actionSortAscending->setCheckable(true);
523 m_actionSortAscending->setChecked(true);
524 m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup);
525 m_actionSortDescending->setCheckable(true);
526 // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-)
527 connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
528 connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent);
529 connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
531 QActionGroup *sortColumnGroup = new QActionGroup(this);
532 m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup);
533 m_actionSortNone->setCheckable(true);
534 m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup);
535 m_actionSortThreading->setCheckable(true);
536 m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup);
537 m_actionSortByArrival->setCheckable(true);
538 m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup);
539 m_actionSortByCc->setCheckable(true);
540 m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup);
541 m_actionSortByDate->setCheckable(true);
542 m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup);
543 m_actionSortByFrom->setCheckable(true);
544 m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup);
545 m_actionSortBySize->setCheckable(true);
546 m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup);
547 m_actionSortBySubject->setCheckable(true);
548 m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup);
549 m_actionSortByTo->setCheckable(true);
550 connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged);
551 slotSortingConfirmed(-1, Qt::AscendingOrder);
553 actionHideRead = new QAction(tr("&Hide Read Messages"), this);
554 actionHideRead->setCheckable(true);
555 if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) {
556 actionHideRead->setChecked(true);
557 prettyMsgListModel->setHideRead(true);
559 connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead);
561 QActionGroup *layoutGroup = new QActionGroup(this);
562 m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup);
563 m_actionLayoutCompact->setCheckable(true);
564 m_actionLayoutCompact->setChecked(true);
565 connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact);
566 m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup);
567 m_actionLayoutWide->setCheckable(true);
568 connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide);
569 m_actionLayoutOneAtTime = new QAction(tr("&One At a Time"), layoutGroup);
570 m_actionLayoutOneAtTime->setCheckable(true);
571 connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime);
574 m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this);
575 m_actionShowOnlySubscribed->setCheckable(true);
576 m_actionShowOnlySubscribed->setEnabled(false);
577 connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed);
578 m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this);
579 m_actionSubscribeMailbox->setCheckable(true);
580 m_actionSubscribeMailbox->setEnabled(false);
581 connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox);
583 aboutTrojita = new QAction(tr("&About Trojitá..."), this);
584 connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita);
586 donateToTrojita = new QAction(tr("&Donate to the project"), this);
587 connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita);
589 connectModelActions();
591 m_composeMenu = new QMenu(tr("Compose Mail"), this);
592 m_composeMenu->addAction(composeMail);
593 m_composeMenu->addAction(m_editDraft);
594 m_composeButton = new QToolButton(this);
595 m_composeButton->setPopupMode(QToolButton::MenuButtonPopup);
596 m_composeButton->setMenu(m_composeMenu);
597 m_composeButton->setDefaultAction(composeMail);
599 m_replyButton = new QToolButton(this);
600 m_replyButton->setPopupMode(QToolButton::MenuButtonPopup);
601 m_replyMenu = new QMenu(m_replyButton);
602 m_replyMenu->addAction(m_replyPrivate);
603 m_replyMenu->addAction(m_replyAllButMe);
604 m_replyMenu->addAction(m_replyAll);
605 m_replyMenu->addAction(m_replyList);
606 m_replyButton->setMenu(m_replyMenu);
607 m_replyButton->setDefaultAction(m_replyPrivate);
609 m_mainToolbar->addWidget(m_composeButton);
610 m_mainToolbar->addWidget(m_replyButton);
611 m_mainToolbar->addAction(m_forwardAsAttachment);
612 m_mainToolbar->addAction(expunge);
613 m_mainToolbar->addSeparator();
614 m_mainToolbar->addAction(markAsRead);
615 m_mainToolbar->addAction(markAsDeleted);
616 m_mainToolbar->addAction(markAsFlagged);
617 m_mainToolbar->addAction(markAsJunk);
618 m_mainToolbar->addAction(markAsNotJunk);
619 m_mainToolbar->addAction(moveToArchive);
621 // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom.
622 QWidget *toolbarSpacer = new QWidget(m_mainToolbar);
623 toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
624 m_mainToolbar->addWidget(toolbarSpacer);
626 m_mainToolbar->addSeparator();
627 m_mainToolbar->addWidget(busyParsersIndicator);
629 networkIndicator = new QToolButton(this);
630 // This is deliberate; we want to show this button in the same style as the other ones in the toolbar
631 networkIndicator->setPopupMode(QToolButton::MenuButtonPopup);
632 m_mainToolbar->addWidget(networkIndicator);
634 m_menuFromToolBar = new QToolButton(this);
635 m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new")));
636 m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON'
637 m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup);
638 connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu);
639 m_mainToolbar->addWidget(m_menuFromToolBar);
640 connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) {
641 // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton
642 m_mainToolbar->actions().last()->setVisible(!menuBarVisible);
645 busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize());
648 // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified.
649 // That sucks, because some of our widgets use multiple actions with an expanding arrow at right.
650 // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows
651 // are, well, at right.
652 // I have no idea how this works on RTL layouts.
653 QLayout *lay = m_mainToolbar->layout();
654 for (int i = 0; i < lay->count(); ++i) {
655 QLayoutItem *it = lay->itemAt(i);
656 if (it->widget() == toolbarSpacer) {
657 // Don't align this one, otherwise it won't push stuff when in horizontal direction
658 continue;
660 if (it->widget() == busyParsersIndicator) {
661 // It looks much better when centered
662 it->setAlignment(Qt::AlignJustify);
663 continue;
665 it->setAlignment(Qt::AlignLeft);
669 updateMessageFlags();
672 void MainWindow::connectModelActions()
674 connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList);
675 connect(netOffline, &QAction::triggered,
676 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline);
677 connect(netExpensive, &QAction::triggered,
678 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive);
679 connect(netOnline, &QAction::triggered,
680 qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline);
681 netExpensive->setEnabled(imapAccess()->isConfigured());
682 netOnline->setEnabled(imapAccess()->isConfigured());
685 void MainWindow::createMenus()
687 #define ADD_ACTION(MENU, ACTION) \
688 MENU->addAction(ACTION); \
689 addAction(ACTION);
691 QMenu *applicationMenu = menuBar()->addMenu(tr("&Application"));
692 ADD_ACTION(applicationMenu, m_actionContactEditor);
693 QMenu *netPolicyMenu = applicationMenu->addMenu(tr("&Network Access"));
694 ADD_ACTION(netPolicyMenu, netOffline);
695 ADD_ACTION(netPolicyMenu, netExpensive);
696 ADD_ACTION(netPolicyMenu, netOnline);
697 QMenu *debugMenu = applicationMenu->addMenu(tr("&Debugging"));
698 ADD_ACTION(debugMenu, showFullView);
699 ADD_ACTION(debugMenu, showTaskView);
700 ADD_ACTION(debugMenu, showMimeView);
701 ADD_ACTION(debugMenu, showProtocolLogger);
702 ADD_ACTION(debugMenu, logPersistent);
703 debugMenu->addSeparator();
704 ADD_ACTION(debugMenu, showImapCapabilities);
705 debugMenu->addSeparator();
706 ADD_ACTION(debugMenu, reloadAllMailboxes);
707 ADD_ACTION(debugMenu, resyncMbox);
708 applicationMenu->addSeparator();
709 ADD_ACTION(applicationMenu, configSettings);
710 ADD_ACTION(applicationMenu, ShortcutHandler::instance()->shortcutConfigAction());
711 applicationMenu->addSeparator();
712 ADD_ACTION(applicationMenu, aboutTrojita);
713 ADD_ACTION(applicationMenu, donateToTrojita);
714 applicationMenu->addSeparator();
715 ADD_ACTION(applicationMenu, exitAction);
717 QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
718 ADD_ACTION(viewMenu, showMenuBar);
719 ADD_ACTION(viewMenu, showToolBar);
720 QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout"));
721 ADD_ACTION(layoutMenu, m_actionLayoutCompact);
722 ADD_ACTION(layoutMenu, m_actionLayoutWide);
723 ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime);
724 viewMenu->addSeparator();
725 ADD_ACTION(viewMenu, m_actionShowOnlySubscribed);
727 QMenu *mailboxMenu = menuBar()->addMenu(tr("Mail&box"));
728 QMenu *sortMenu = mailboxMenu->addMenu(tr("S&orting"));
729 ADD_ACTION(sortMenu, m_actionSortNone);
730 ADD_ACTION(sortMenu, m_actionSortThreading);
731 ADD_ACTION(sortMenu, m_actionSortByArrival);
732 ADD_ACTION(sortMenu, m_actionSortByCc);
733 ADD_ACTION(sortMenu, m_actionSortByDate);
734 ADD_ACTION(sortMenu, m_actionSortByFrom);
735 ADD_ACTION(sortMenu, m_actionSortBySize);
736 ADD_ACTION(sortMenu, m_actionSortBySubject);
737 ADD_ACTION(sortMenu, m_actionSortByTo);
738 sortMenu->addSeparator();
739 ADD_ACTION(sortMenu, m_actionSortAscending);
740 ADD_ACTION(sortMenu, m_actionSortDescending);
741 sortMenu->addSeparator();
742 ADD_ACTION(sortMenu, actionThreadMsgList);
743 ADD_ACTION(sortMenu, actionHideRead);
744 mailboxMenu->addSeparator();
745 ADD_ACTION(mailboxMenu, m_previousMessage);
746 ADD_ACTION(mailboxMenu, m_nextMessage);
747 mailboxMenu->addSeparator();
748 ADD_ACTION(mailboxMenu, expunge);
750 QMenu *messageMenu = menuBar()->addMenu(tr("&Message"));
751 ADD_ACTION(messageMenu, composeMail);
752 ADD_ACTION(messageMenu, m_editDraft);
753 messageMenu->addSeparator();
754 ADD_ACTION(messageMenu, m_replyGuess);
755 ADD_ACTION(messageMenu, m_replyPrivate);
756 ADD_ACTION(messageMenu, m_replyAll);
757 ADD_ACTION(messageMenu, m_replyAllButMe);
758 ADD_ACTION(messageMenu, m_replyList);
759 messageMenu->addSeparator();
760 ADD_ACTION(messageMenu, m_forwardAsAttachment);
761 ADD_ACTION(messageMenu, m_resend);
763 QMenu *mainMenuBehindToolBar = new QMenu(this);
764 m_menuFromToolBar->setMenu(mainMenuBehindToolBar);
765 m_menuFromToolBar->menu()->addMenu(applicationMenu);
766 m_menuFromToolBar->menu()->addMenu(viewMenu);
767 m_menuFromToolBar->menu()->addMenu(mailboxMenu);
768 m_menuFromToolBar->menu()->addMenu(messageMenu);
769 m_menuFromToolBar->menu()->addSeparator();
770 m_menuFromToolBar->menu()->addAction(showMenuBar);
772 networkIndicator->setMenu(netPolicyMenu);
773 m_netToolbarDefaultAction = new QAction(this);
774 networkIndicator->setDefaultAction(m_netToolbarDefaultAction);
775 connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu);
776 connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
777 connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
778 connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication);
780 addToolBar(Qt::LeftToolBarArea, m_mainToolbar);
781 m_mainToolbar->actions().last()->setVisible(true); // initial state to complement the default of the QMenuBar's visibility
782 menuBar()->hide();
784 #undef ADD_ACTION
787 void MainWindow::createWidgets()
789 // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure
790 // we do not hit the disk continually when e.g. resizing some random widget.
791 m_delayedStateSaving = new QTimer(this);
792 m_delayedStateSaving->setInterval(1000);
793 m_delayedStateSaving->setSingleShot(true);
794 connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState);
796 mboxTree = new MailBoxTreeView(nullptr, m_settings);
797 mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList());
798 connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree);
799 connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) {
800 m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames);
803 msgListWidget = new MessageListWidget(nullptr, m_favoriteTags);
804 msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu);
805 msgListWidget->tree->setAlternatingRowColors(true);
806 msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool());
807 connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting);
809 connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree);
810 connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked);
811 connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked);
812 connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::openCompleteMessageWidget);
813 connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested);
814 connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
815 connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
817 msgListWidget->tree->installEventFilter(this);
819 m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager, m_favoriteTags);
820 connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp);
821 connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions);
822 #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
823 connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) {
824 if (url.isEmpty()) {
825 QToolTip::hideText();
826 } else {
827 // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783
828 QTimer::singleShot(250, [url]() {
829 QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url)));
833 #endif
834 connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError);
835 // Do not try to get onto the homepage when we are on EXPENSIVE connection
836 if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() &&
837 m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) {
838 m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version)));
841 allDock = new QDockWidget(tr("Everything"), this);
842 allDock->setObjectName(QStringLiteral("allDock"));
843 allTree = new QTreeView(allDock);
844 allDock->hide();
845 allTree->setUniformRowHeights(true);
846 allTree->setHeaderHidden(true);
847 allDock->setWidget(allTree);
848 addDockWidget(Qt::LeftDockWidgetArea, allDock);
849 taskDock = new QDockWidget(tr("IMAP Tasks"), this);
850 taskDock->setObjectName(QStringLiteral("taskDock"));
851 taskTree = new QTreeView(taskDock);
852 taskDock->hide();
853 taskTree->setHeaderHidden(true);
854 taskDock->setWidget(taskTree);
855 addDockWidget(Qt::LeftDockWidgetArea, taskDock);
856 mailMimeDock = new QDockWidget(tr("MIME Tree"), this);
857 mailMimeDock->setObjectName(QStringLiteral("mailMimeDock"));
858 mailMimeTree = new QTreeView(mailMimeDock);
859 mailMimeDock->hide();
860 mailMimeTree->setUniformRowHeights(true);
861 mailMimeTree->setHeaderHidden(true);
862 mailMimeDock->setWidget(mailMimeTree);
863 addDockWidget(Qt::RightDockWidgetArea, mailMimeDock);
864 connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged);
866 protocolLoggerDock = new QDockWidget(tr("Protocol log"), this);
867 protocolLoggerDock->setObjectName(QStringLiteral("protocolLoggerDock"));
868 protocolLogger = new ProtocolLoggerWidget(protocolLoggerDock);
869 protocolLoggerDock->hide();
870 protocolLoggerDock->setWidget(protocolLogger);
871 addDockWidget(Qt::BottomDockWidgetArea, protocolLoggerDock);
873 busyParsersIndicator = new TaskProgressIndicator(this);
876 void MainWindow::setupModels()
878 m_imapAccess->reloadConfiguration();
879 m_imapAccess->doConnect();
881 m_messageWidget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
883 auto realThreadingModel = qobject_cast<Imap::Mailbox::ThreadingMsgListModel*>(m_imapAccess->threadingMsgListModel());
884 Q_ASSERT(realThreadingModel);
885 auto realMsgListModel = qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel());
886 Q_ASSERT(realMsgListModel);
888 prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast<QAbstractItemModel *>(m_imapAccess->mailboxModel()));
889 prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel"));
890 connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed,
891 msgListWidget, &MessageListWidget::slotSortingFailed);
892 prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this);
893 prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel());
894 prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel"));
896 connect(mboxTree, &MailBoxTreeView::clicked,
897 realMsgListModel,
898 static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
899 connect(mboxTree, &MailBoxTreeView::activated,
900 realMsgListModel,
901 static_cast<void (Imap::Mailbox::MsgListModel::*)(const QModelIndex &)>(&Imap::Mailbox::MsgListModel::setMailbox));
902 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags);
903 connect(qobject_cast<Imap::Mailbox::MsgListModel*>(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable,
904 this, &MainWindow::slotScrollToUnseenMessage);
905 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
906 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
907 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags);
908 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
909 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags);
910 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch);
911 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags);
912 connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged);
914 connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived);
915 connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError);
916 connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError);
917 connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection);
918 connect(imapModel(), &Imap::Mailbox::Model::authAttemptFailed, this, [this]() {
919 m_ignoreStoredPassword = true;
922 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline);
923 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive);
924 connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline);
925 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, [this](uint, const Imap::ConnectionState state) {
926 if (state == Imap::CONN_STATE_AUTHENTICATED) {
927 m_ignoreStoredPassword = false;
930 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus);
932 connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed);
933 connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed);
934 connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed);
936 connect(imapModel(), &Imap::Mailbox::Model::logged, protocolLogger, &ProtocolLoggerWidget::log);
937 connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, protocolLogger, &ProtocolLoggerWidget::onConnectionClosed);
939 auto nw = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher());
940 Q_ASSERT(nw);
941 connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled,
942 this, [this](const int timeout) {
943 showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000));
945 connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState);
947 connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage);
949 connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated);
951 connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle);
952 connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle);
954 connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed);
956 //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this );
957 //w->setModel( imapModel() );
959 //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time
961 mboxTree->setModel(prettyMboxModel);
962 msgListWidget->tree->setModel(prettyMsgListModel);
963 connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags);
965 allTree->setModel(imapModel());
966 taskTree->setModel(imapModel()->taskModel());
967 connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll);
968 connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll);
969 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll);
970 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll);
971 connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll);
973 busyParsersIndicator->setImapModel(imapModel());
975 auto accountIconName = m_settings->value(Common::SettingsNames::imapAccountIcon).toString();
976 if (accountIconName.isEmpty()) {
977 qApp->setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita")));
978 } else if (accountIconName.contains(QDir::separator())) {
979 // Absolute paths are OK for users, but unsupported by our icon loader
980 qApp->setWindowIcon(QIcon(accountIconName));
981 } else {
982 qApp->setWindowIcon(UiUtils::loadIcon(accountIconName));
986 void MainWindow::createSysTray()
988 if (m_trayIcon)
989 return;
991 qApp->setQuitOnLastWindowClosed(false);
993 m_trayIcon = new QSystemTrayIcon(this);
994 handleTrayIconChange();
996 QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon);
997 connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
999 QMenu *trayIconMenu = new QMenu(this);
1000 trayIconMenu->addAction(quitAction);
1001 m_trayIcon->setContextMenu(trayIconMenu);
1003 // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope.
1004 connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater);
1006 connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated);
1007 connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange);
1008 m_trayIcon->setVisible(true);
1009 m_trayIcon->show();
1012 void MainWindow::removeSysTray()
1014 delete m_trayIcon;
1015 m_trayIcon = 0;
1017 qApp->setQuitOnLastWindowClosed(true);
1020 void MainWindow::slotToggleSysTray()
1022 bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool();
1023 if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) {
1024 createSysTray();
1025 } else if (!showSystray && m_trayIcon) {
1026 removeSysTray();
1030 void MainWindow::handleTrayIconChange()
1032 if (!m_trayIcon)
1033 return;
1035 const bool isOffline = qobject_cast<Imap::Mailbox::NetworkWatcher *>(m_imapAccess->networkWatcher())->effectiveNetworkPolicy()
1036 == Imap::Mailbox::NETWORK_OFFLINE;
1037 auto pixmap = qApp->windowIcon()
1038 .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal);
1039 QString tooltip;
1040 auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
1041 if (profileName.isEmpty()) {
1042 tooltip = QStringLiteral("Trojitá");
1043 } else {
1044 tooltip = QStringLiteral("Trojitá [%1]").arg(profileName);
1047 int unreadCount = 0;
1048 bool numbersValid = false;
1050 auto watchingMode = settings()->value(Common::SettingsNames::watchedFoldersKey).toString();
1051 if (watchingMode == Common::SettingsNames::watchAll || watchingMode == Common::SettingsNames::watchSubscribed) {
1052 bool subscribedOnly = watchingMode == Common::SettingsNames::watchSubscribed;
1053 unreadCount = std::accumulate(UiUtils::QaimDfsIterator(m_imapAccess->mailboxModel()->index(0, 0), m_imapAccess->mailboxModel()),
1054 UiUtils::QaimDfsIterator(), 0, [subscribedOnly](const int acc, const QModelIndex &idx) {
1056 if (subscribedOnly && !idx.data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool())
1057 return acc;
1059 auto x = idx.data(Imap::Mailbox::RoleUnreadMessageCount).toInt();
1060 if (x > 0) {
1061 return acc + x;
1062 } else {
1063 return acc;
1066 // only show stuff if there are some mailboxes, and if there are such messages
1067 numbersValid = m_imapAccess->mailboxModel()->hasChildren() && unreadCount > 0;
1069 } else {
1070 // just for the INBOX
1071 QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex());
1072 if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX")
1073 && mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) {
1074 unreadCount = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt();
1075 numbersValid = true;
1079 if (numbersValid) {
1080 QFont f;
1081 f.setPixelSize(pixmap.height() * 0.59);
1082 f.setWeight(QFont::Bold);
1084 QString text = QString::number(unreadCount);
1085 QFontMetrics fm(f);
1086 if (unreadCount > 666) {
1087 // You just have too many messages.
1088 text = QStringLiteral("🐮");
1089 fm = QFontMetrics(f);
1090 } else if (fm.width(text) > pixmap.width()) {
1091 f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text));
1092 fm = QFontMetrics(f);
1095 QRect boundingRect = fm.tightBoundingRect(text);
1096 boundingRect.setWidth(boundingRect.width() + 2);
1097 boundingRect.setHeight(boundingRect.height() + 2);
1098 boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2));
1099 boundingRect = boundingRect.intersected(pixmap.rect());
1101 QPainterPath path;
1102 path.addText(boundingRect.bottomLeft(), f, text);
1104 QPainter painter(&pixmap);
1105 painter.setRenderHint(QPainter::Antialiasing);
1106 painter.setPen(QColor(255,255,255, 180));
1107 painter.setBrush(isOffline ? Qt::red : Qt::black);
1108 painter.drawPath(path);
1110 //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]"
1111 tooltip += tr(" - %n unread message(s)", 0, unreadCount);
1112 } else if (isOffline) {
1113 //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]"
1114 tooltip += tr(" - offline");
1116 m_trayIcon->setToolTip(tooltip);
1117 m_trayIcon->setIcon(QIcon(pixmap));
1120 void MainWindow::closeEvent(QCloseEvent *event)
1122 if (m_trayIcon && m_trayIcon->isVisible()) {
1123 Util::askForSomethingUnlessTold(tr("Trojitá"),
1124 tr("The application will continue in systray. This can be disabled within the settings."),
1125 Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings);
1126 hide();
1127 event->ignore();
1131 bool MainWindow::eventFilter(QObject *o, QEvent *e)
1133 if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) {
1134 if (e->type() == QEvent::KeyPress) {
1135 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
1136 if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) {
1137 QCoreApplication::sendEvent(m_messageWidget, keyEvent);
1138 return true;
1140 return false;
1142 return false;
1144 if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) {
1145 // installed if sorting is not really possible.
1146 QWidget *header = static_cast<QWidget*>(o);
1147 QMouseEvent *mouse = static_cast<QMouseEvent*>(e);
1148 if (e->type() == QEvent::MouseButtonPress) {
1149 if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) {
1150 m_headerDragStart = mouse->pos();
1152 return false;
1154 if (e->type() == QEvent::MouseButtonRelease) {
1155 if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor &&
1156 (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) {
1157 m_actionSortDescending->toggle();
1158 Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
1159 msgListWidget->tree->header()->setSortIndicator(-1, order);
1160 return true; // prevent regular click
1164 return false;
1167 void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason)
1169 if (reason == QSystemTrayIcon::Trigger) {
1170 setVisible(!isVisible());
1171 if (isVisible())
1172 showMainWindow();
1176 void MainWindow::showMainWindow()
1178 setVisible(true);
1179 activateWindow();
1180 raise();
1183 void MainWindow::msgListClicked(const QModelIndex &index)
1185 Q_ASSERT(index.isValid());
1187 if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier)
1188 return;
1190 if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1191 return;
1193 // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that
1194 // user will be notified about new arrivals, etc.
1195 QModelIndex translated = Imap::deproxifiedIndex(index);
1196 imapModel()->switchToMailbox(translated.parent().parent());
1198 if (index.column() == Imap::Mailbox::MsgListModel::SEEN) {
1199 if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1200 return;
1201 Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ?
1202 Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1203 imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp);
1205 if (translated == m_messageWidget->messageView->currentMessage()) {
1206 m_messageWidget->messageView->stopAutoMarkAsRead();
1208 } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) {
1209 if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool())
1210 return;
1212 Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ?
1213 Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD;
1214 imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp);
1215 } else {
1216 if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) {
1217 // isVisible() won't work, the splitter manipulates width, not the visibility state
1218 m_messageWidget->messageView->setMessage(index);
1220 msgListWidget->tree->setCurrentIndex(index);
1224 void MainWindow::openCompleteMessageWidget()
1226 const QModelIndex index = msgListWidget->tree->currentIndex();
1228 if (! index.data(Imap::Mailbox::RoleMessageUid).isValid())
1229 return;
1231 CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager, m_favoriteTags);
1232 widget->messageView->setMessage(index);
1233 widget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(m_imapAccess->networkWatcher()));
1234 widget->setFocusPolicy(Qt::StrongFocus);
1235 widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString());
1236 widget->setAttribute(Qt::WA_DeleteOnClose);
1237 QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget);
1238 widget->addAction(closeAction);
1239 widget->show();
1242 void MainWindow::showContextMenuMboxTree(const QPoint &position)
1244 QList<QAction *> actionList;
1245 if (mboxTree->indexAt(position).isValid()) {
1246 actionList.append(createChildMailbox);
1247 actionList.append(deleteCurrentMailbox);
1248 actionList.append(m_actionMarkMailboxAsRead);
1249 actionList.append(resyncMbox);
1250 actionList.append(reloadMboxList);
1252 actionList.append(m_actionSubscribeMailbox);
1253 m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool());
1255 #ifdef XTUPLE_CONNECT
1256 actionList.append(xtIncludeMailboxInSync);
1257 xtIncludeMailboxInSync->setChecked(
1258 m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains(
1259 mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString()));
1260 #endif
1261 } else {
1262 actionList.append(createTopMailbox);
1264 actionList.append(reloadAllMailboxes);
1265 actionList.append(m_actionShowOnlySubscribed);
1266 QMenu::exec(actionList, mboxTree->mapToGlobal(position), nullptr, this);
1269 void MainWindow::showContextMenuMsgListTree(const QPoint &position)
1271 QList<QAction *> actionList;
1272 QModelIndex index = msgListWidget->tree->indexAt(position);
1273 if (index.isValid()) {
1274 updateMessageFlagsOf(index);
1275 actionList.append(markAsRead);
1276 actionList.append(markAsDeleted);
1277 actionList.append(markAsFlagged);
1278 actionList.append(markAsJunk);
1279 actionList.append(markAsNotJunk);
1280 actionList.append(moveToArchive);
1281 actionList.append(m_actionMarkMailboxAsRead);
1282 actionList.append(saveWholeMessage);
1283 actionList.append(viewMsgSource);
1284 actionList.append(viewMsgHeaders);
1285 auto appendTagIfExists = [this,&actionList](const int row, QAction *tag) {
1286 if (m_favoriteTags->rowCount() > row - 1)
1287 actionList.append(tag);
1289 appendTagIfExists(1, tag1);
1290 appendTagIfExists(2, tag2);
1291 appendTagIfExists(3, tag3);
1292 appendTagIfExists(4, tag4);
1293 appendTagIfExists(5, tag5);
1294 appendTagIfExists(6, tag6);
1295 appendTagIfExists(7, tag7);
1296 appendTagIfExists(8, tag8);
1297 appendTagIfExists(9, tag9);
1299 if (! actionList.isEmpty())
1300 QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position), nullptr, this);
1303 /** @short Ask for an updated list of mailboxes situated below the selected one
1306 void MainWindow::slotReloadMboxList()
1308 Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1309 Q_ASSERT(item.isValid());
1310 if (item.column() != 0)
1311 continue;
1312 Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1313 Imap::Mailbox::Model::realTreeItem(item)
1315 Q_ASSERT(mbox);
1316 mbox->rescanForChildMailboxes(imapModel());
1320 /** @short Request a check for new messages in selected mailbox */
1321 void MainWindow::slotResyncMbox()
1323 if (! imapModel()->isNetworkAvailable())
1324 return;
1326 Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) {
1327 Q_ASSERT(item.isValid());
1328 if (item.column() != 0)
1329 continue;
1330 imapModel()->resyncMailbox(item);
1334 void MainWindow::alertReceived(const QString &message)
1336 //: "ALERT" is a special warning which we're required to show to the user
1337 Gui::Util::messageBoxWarning(this, tr("IMAP Alert"), message);
1340 void MainWindow::imapError(const QString &message)
1342 Gui::Util::messageBoxCritical(this, tr("IMAP Protocol Error"), message);
1343 // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report.
1344 // </joke>
1345 showProtocolLogger->setChecked(true);
1348 void MainWindow::networkError(const QString &message)
1350 const QString title = tr("Network Error");
1351 if (!m_networkErrorMessageBox) {
1352 m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title,
1353 QString(), QMessageBox::Ok, this);
1355 // User must be informed about a new (but not recurring) error
1356 if (message != m_networkErrorMessageBox->text()) {
1357 m_networkErrorMessageBox->setText(message);
1358 if (qApp->applicationState() == Qt::ApplicationActive) {
1359 m_networkErrorMessageBox->setProperty(netErrorUnseen, false);
1360 m_networkErrorMessageBox->show();
1361 } else {
1362 m_networkErrorMessageBox->setProperty(netErrorUnseen, true);
1363 if (m_trayIcon && m_trayIcon->isVisible())
1364 m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333);
1369 void MainWindow::cacheError(const QString &message)
1371 Gui::Util::messageBoxCritical(this, tr("IMAP Cache Error"),
1372 tr("The caching subsystem managing a cache of the data already "
1373 "downloaded from the IMAP server is having troubles. "
1374 "All caching will be disabled.\n\n%1").arg(message));
1377 void MainWindow::networkPolicyOffline()
1379 netExpensive->setChecked(false);
1380 netOnline->setChecked(false);
1381 netOffline->setChecked(true);
1382 updateActionsOnlineOffline(false);
1383 showStatusMessage(tr("Offline"));
1384 handleTrayIconChange();
1387 void MainWindow::networkPolicyExpensive()
1389 netOffline->setChecked(false);
1390 netOnline->setChecked(false);
1391 netExpensive->setChecked(true);
1392 updateActionsOnlineOffline(true);
1393 handleTrayIconChange();
1396 void MainWindow::networkPolicyOnline()
1398 netOffline->setChecked(false);
1399 netExpensive->setChecked(false);
1400 netOnline->setChecked(true);
1401 updateActionsOnlineOffline(true);
1402 handleTrayIconChange();
1405 /** @short Deletes a network error message box instance upon resetting of reconnect state */
1406 void MainWindow::slotResetReconnectState()
1408 if (m_networkErrorMessageBox) {
1409 delete m_networkErrorMessageBox;
1410 m_networkErrorMessageBox = 0;
1414 void MainWindow::slotShowSettings()
1416 SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_favoriteTags, m_settings);
1417 if (dialog->exec() == QDialog::Accepted) {
1418 // FIXME: wipe cache in case we're moving between servers
1419 nukeModels();
1420 setupModels();
1421 connectModelActions();
1422 // The systray is still connected to the old model -- got to make sure it's getting updated
1423 removeSysTray();
1424 slotToggleSysTray();
1426 QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString();
1427 if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL &&
1428 method != Common::SettingsNames::methodProcess ) {
1429 Gui::Util::messageBoxCritical(this, tr("No Configuration"),
1430 tr("No IMAP account is configured. Trojitá cannot do much without one."));
1432 applySizesAndState();
1435 void MainWindow::authenticationRequested()
1437 Plugins::PasswordPlugin *password = pluginManager()->password();
1438 if (password) {
1439 // FIXME: use another account-id at some point in future
1440 // Currently the accountName will be empty unless Trojita has been
1441 // called with a profile, and then the profile will be used as the
1442 // accountName.
1443 QString accountName = m_imapAccess->accountName();
1444 if (accountName.isEmpty())
1445 accountName = QStringLiteral("account-0");
1446 Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap"));
1447 if (job) {
1448 connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) {
1449 authenticationContinue(password);
1451 connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) {
1452 if (error == Plugins::PasswordJob::Error::NoSuchPassword) {
1453 authenticationContinue(QString());
1454 } else {
1455 authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message));
1458 job->setAutoDelete(true);
1459 job->start();
1460 return;
1464 authenticationContinue(QString());
1468 void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage)
1470 const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString();
1471 QString pass = password;
1472 if (m_ignoreStoredPassword || pass.isEmpty()) {
1473 auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"),
1474 tr("<p>Please provide IMAP password for user <b>%1</b> on <b>%2</b>:</p>").arg(
1475 user.toHtmlEscaped(),
1476 m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped()
1478 errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n"))
1479 + imapModel()->imapAuthError());
1480 connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword);
1481 connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword);
1482 } else {
1483 imapModel()->setImapPassword(pass);
1487 void MainWindow::checkSslPolicy()
1489 m_imapAccess->setSslPolicy(QMessageBox(static_cast<QMessageBox::Icon>(m_imapAccess->sslInfoIcon()),
1490 m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(),
1491 QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes);
1494 void MainWindow::nukeModels()
1496 m_messageWidget->messageView->setEmpty();
1497 mboxTree->setModel(0);
1498 msgListWidget->tree->setModel(0);
1499 allTree->setModel(0);
1500 taskTree->setModel(0);
1501 delete prettyMsgListModel;
1502 prettyMsgListModel = 0;
1503 delete prettyMboxModel;
1504 prettyMboxModel = 0;
1507 void MainWindow::recoverDrafts()
1509 QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/"));
1510 QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft")));
1511 Q_FOREACH(const QString &draft, drafts) {
1512 ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this);
1513 // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured
1514 if (!w)
1515 break;
1519 void MainWindow::slotComposeMail()
1521 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this);
1524 void MainWindow::slotEditDraft()
1526 QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts"));
1527 QDir().mkpath(path);
1528 path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)"));
1529 if (!path.isNull()) {
1530 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this);
1534 QModelIndexList MainWindow::translatedSelection() const
1536 QModelIndexList translatedIndexes;
1537 Q_FOREACH(const QModelIndex &index, msgListWidget->tree->selectedTree()) {
1538 translatedIndexes << Imap::deproxifiedIndex(index);
1540 return translatedIndexes;
1543 void MainWindow::handleMarkAsRead(bool value)
1545 const QModelIndexList translatedIndexes = translatedSelection();
1546 if (translatedIndexes.isEmpty()) {
1547 qDebug() << "Model::handleMarkAsRead: no valid messages";
1548 } else {
1549 imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1550 if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) {
1551 m_messageWidget->messageView->stopAutoMarkAsRead();
1556 void MainWindow::slotNextUnread()
1558 QModelIndex current = msgListWidget->tree->currentIndex();
1560 UiUtils::gotoNext(msgListWidget->tree->model(), current,
1561 [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); },
1562 [this](const QModelIndex &idx) {
1563 Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool());
1564 m_messageWidget->messageView->setMessage(idx);
1565 msgListWidget->tree->setCurrentIndex(idx);
1567 []() {
1568 // nothing to do
1572 void MainWindow::slotPreviousUnread()
1574 QModelIndex current = msgListWidget->tree->currentIndex();
1576 UiUtils::gotoPrevious(msgListWidget->tree->model(), current,
1577 [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); },
1578 [this](const QModelIndex &idx) {
1579 Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool());
1580 m_messageWidget->messageView->setMessage(idx);
1581 msgListWidget->tree->setCurrentIndex(idx);
1583 []() {
1584 // nothing to do
1588 void MainWindow::handleTag(const bool checked, const int index)
1590 const QModelIndexList &translatedIndexes = translatedSelection();
1591 if (translatedIndexes.isEmpty()) {
1592 qDebug() << "Model::handleTag: no valid messages";
1593 } else {
1594 const auto &tagName = m_favoriteTags->tagNameByIndex(index);
1595 if (!tagName.isEmpty())
1596 imapModel()->setMessageFlags(translatedIndexes, tagName, checked ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1600 void MainWindow::handleMarkAsDeleted(bool value)
1602 const QModelIndexList translatedIndexes = translatedSelection();
1603 if (translatedIndexes.isEmpty()) {
1604 qDebug() << "Model::handleMarkAsDeleted: no valid messages";
1605 } else {
1606 imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1610 void MainWindow::handleMarkAsFlagged(const bool value)
1612 const QModelIndexList translatedIndexes = translatedSelection();
1613 if (translatedIndexes.isEmpty()) {
1614 qDebug() << "Model::handleMarkAsFlagged: no valid messages";
1615 } else {
1616 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1620 void MainWindow::handleMarkAsJunk(const bool value)
1622 const QModelIndexList translatedIndexes = translatedSelection();
1623 if (translatedIndexes.isEmpty()) {
1624 qDebug() << "Model::handleMarkAsJunk: no valid messages";
1625 } else {
1626 if (value) {
1627 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE);
1629 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1633 void MainWindow::handleMarkAsNotJunk(const bool value)
1635 const QModelIndexList translatedIndexes = translatedSelection();
1636 if (translatedIndexes.isEmpty()) {
1637 qDebug() << "Model::handleMarkAsNotJunk: no valid messages";
1638 } else {
1639 if (value) {
1640 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE);
1642 imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE);
1646 void MainWindow::slotMoveToArchiveFailed(const QString &error)
1648 // XXX disable busy cursor
1649 QMessageBox::critical(this, tr("Failed to archive"), error);
1652 void MainWindow::handleMoveToArchive()
1654 const QModelIndexList translatedIndexes = translatedSelection();
1655 if (translatedIndexes.isEmpty()) {
1656 qDebug() << "Model::handleMoveToArchive: no valid messages";
1657 } else {
1658 auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString();
1659 auto copyMoveMessagesTask = imapModel()->copyMoveMessages(
1660 archiveFolderName.isEmpty() ? Common::SettingsNames::imapDefaultArchiveFolderName : archiveFolderName,
1661 translatedIndexes, Imap::Mailbox::CopyMoveOperation::MOVE);
1662 connect(copyMoveMessagesTask, &Imap::Mailbox::ImapTask::failed, this, &MainWindow::slotMoveToArchiveFailed);
1667 void MainWindow::slotExpunge()
1669 imapModel()->expungeMailbox(qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox());
1672 void MainWindow::slotMarkCurrentMailboxRead()
1674 imapModel()->markMailboxAsRead(mboxTree->currentIndex());
1677 void MainWindow::slotCreateMailboxBelowCurrent()
1679 createMailboxBelow(mboxTree->currentIndex());
1682 void MainWindow::slotCreateTopMailbox()
1684 createMailboxBelow(QModelIndex());
1687 void MainWindow::createMailboxBelow(const QModelIndex &index)
1689 Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ?
1690 dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
1691 Imap::Mailbox::Model::realTreeItem(index)) :
1694 Ui::CreateMailboxDialog ui;
1695 QDialog *dialog = new QDialog(this);
1696 ui.setupUi(dialog);
1698 dialog->setWindowTitle(mboxPtr ?
1699 tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) :
1700 tr("Create a Top-level Mailbox"));
1702 if (dialog->exec() == QDialog::Accepted) {
1703 QStringList parts;
1704 if (mboxPtr)
1705 parts << mboxPtr->mailbox();
1706 parts << ui.mailboxName->text();
1707 if (ui.otherMailboxes->isChecked())
1708 parts << QString();
1709 QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString()); // FIXME: top-level separator
1710 imapModel()->createMailbox(targetName,
1711 ui.subscribe->isChecked() ?
1712 Imap::Mailbox::AutoSubscription::SUBSCRIBE :
1713 Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION
1718 void MainWindow::slotDeleteCurrentMailbox()
1720 if (! mboxTree->currentIndex().isValid())
1721 return;
1723 QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex());
1724 Q_ASSERT(mailbox.isValid());
1725 QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString();
1727 if (QMessageBox::question(this, tr("Delete Mailbox"),
1728 tr("Are you sure to delete mailbox %1?").arg(name),
1729 QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
1730 imapModel()->deleteMailbox(name);
1734 void MainWindow::updateMessageFlags()
1736 updateMessageFlagsOf(QModelIndex());
1739 void MainWindow::updateMessageFlagsOf(const QModelIndex &index)
1741 QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection();
1742 const bool isValid = !indexes.isEmpty() &&
1743 // either we operate on the -already valided- selection or the index must be valid
1744 (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0);
1745 const bool okToModify = imapModel()->isNetworkAvailable() && isValid;
1747 markAsRead->setEnabled(okToModify);
1748 markAsDeleted->setEnabled(okToModify);
1749 markAsFlagged->setEnabled(okToModify);
1750 markAsJunk->setEnabled(okToModify);
1751 markAsNotJunk->setEnabled(okToModify);
1753 // There's no point in moving from Archive to, well, Archive
1754 auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString();
1755 if (archiveFolderName.isEmpty()) {
1756 archiveFolderName = Common::SettingsNames::imapDefaultArchiveFolderName;
1758 moveToArchive->setEnabled(okToModify &&
1759 std::any_of(indexes.cbegin(), indexes.cend(),
1760 [archiveFolderName](const QModelIndex &i) {
1761 return i.data(Imap::Mailbox::RoleMailboxName) != archiveFolderName;
1762 }));
1764 tag1->setEnabled(okToModify);
1765 tag2->setEnabled(okToModify);
1766 tag3->setEnabled(okToModify);
1767 tag4->setEnabled(okToModify);
1768 tag5->setEnabled(okToModify);
1769 tag6->setEnabled(okToModify);
1770 tag7->setEnabled(okToModify);
1771 tag8->setEnabled(okToModify);
1772 tag9->setEnabled(okToModify);
1774 bool isRead = isValid,
1775 isDeleted = isValid,
1776 isFlagged = isValid,
1777 isJunk = isValid,
1778 isNotJunk = isValid,
1779 hasTag1 = isValid,
1780 hasTag2 = isValid,
1781 hasTag3 = isValid,
1782 hasTag4 = isValid,
1783 hasTag5 = isValid,
1784 hasTag6 = isValid,
1785 hasTag7 = isValid,
1786 hasTag8 = isValid,
1787 hasTag9 = isValid;
1788 auto updateTag = [=](const QModelIndex &i, bool &hasTag, int index) {
1789 if (hasTag && !m_favoriteTags->tagNameByIndex(index).isEmpty() &&
1790 !i.data(Imap::Mailbox::RoleMessageFlags).toStringList().contains(m_favoriteTags->tagNameByIndex(index)))
1792 hasTag = false;
1795 Q_FOREACH (const QModelIndex &i, indexes) {
1796 #define UPDATE_STATE(PROP) \
1797 if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \
1798 is##PROP = false;
1799 UPDATE_STATE(Read)
1800 UPDATE_STATE(Deleted)
1801 UPDATE_STATE(Flagged)
1802 UPDATE_STATE(Junk)
1803 UPDATE_STATE(NotJunk)
1804 #undef UPDATE_STATE
1805 updateTag(i, hasTag1, 0);
1806 updateTag(i, hasTag2, 1);
1807 updateTag(i, hasTag3, 2);
1808 updateTag(i, hasTag4, 3);
1809 updateTag(i, hasTag5, 4);
1810 updateTag(i, hasTag6, 5);
1811 updateTag(i, hasTag7, 6);
1812 updateTag(i, hasTag8, 7);
1813 updateTag(i, hasTag9, 8);
1815 markAsRead->setChecked(isRead);
1816 markAsDeleted->setChecked(isDeleted);
1817 markAsFlagged->setChecked(isFlagged);
1818 markAsJunk->setChecked(isJunk && !isNotJunk);
1819 markAsNotJunk->setChecked(isNotJunk && !isJunk);
1821 tag1->setChecked(hasTag1);
1822 tag2->setChecked(hasTag2);
1823 tag3->setChecked(hasTag3);
1824 tag4->setChecked(hasTag4);
1825 tag5->setChecked(hasTag5);
1826 tag6->setChecked(hasTag6);
1827 tag7->setChecked(hasTag7);
1828 tag8->setChecked(hasTag8);
1829 tag9->setChecked(hasTag9);
1832 void MainWindow::updateActionsOnlineOffline(bool online)
1834 reloadMboxList->setEnabled(online);
1835 resyncMbox->setEnabled(online);
1836 expunge->setEnabled(online);
1837 createChildMailbox->setEnabled(online);
1838 createTopMailbox->setEnabled(online);
1839 deleteCurrentMailbox->setEnabled(online);
1840 m_actionMarkMailboxAsRead->setEnabled(online);
1841 updateMessageFlags();
1842 showImapCapabilities->setEnabled(online);
1843 if (!online) {
1844 m_replyGuess->setEnabled(false);
1845 m_replyPrivate->setEnabled(false);
1846 m_replyAll->setEnabled(false);
1847 m_replyAllButMe->setEnabled(false);
1848 m_replyList->setEnabled(false);
1849 m_forwardAsAttachment->setEnabled(false);
1850 m_resend->setEnabled(false);
1854 void MainWindow::slotUpdateMessageActions()
1856 Composer::RecipientList dummy;
1857 m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(),
1858 m_messageWidget->messageView->currentMessage(), dummy));
1859 m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(),
1860 m_messageWidget->messageView->currentMessage(), dummy));
1861 m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(),
1862 m_messageWidget->messageView->currentMessage(), dummy));
1863 m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(),
1864 m_messageWidget->messageView->currentMessage(), dummy));
1865 m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled()
1866 || m_replyAll->isEnabled() || m_replyList->isEnabled());
1868 // Check the default reply mode
1869 // I suspect this is not going to work for everybody. Suggestions welcome...
1870 if (m_replyList->isEnabled()) {
1871 m_replyButton->setDefaultAction(m_replyList);
1872 } else if (m_replyAllButMe->isEnabled()) {
1873 m_replyButton->setDefaultAction(m_replyAllButMe);
1874 } else {
1875 m_replyButton->setDefaultAction(m_replyPrivate);
1878 m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid());
1879 m_resend->setEnabled(m_messageWidget->messageView->currentMessage().isValid());
1882 void MainWindow::scrollMessageUp()
1884 m_messageWidget->area->ensureVisible(0, 0, 0, 0);
1887 void MainWindow::slotReplyTo()
1889 m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE);
1892 void MainWindow::slotReplyAll()
1894 m_messageWidget->messageView->reply(this, Composer::REPLY_ALL);
1897 void MainWindow::slotReplyAllButMe()
1899 m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME);
1902 void MainWindow::slotReplyList()
1904 m_messageWidget->messageView->reply(this, Composer::REPLY_LIST);
1907 void MainWindow::slotReplyGuess()
1909 if (m_replyButton->defaultAction() == m_replyAllButMe) {
1910 slotReplyAllButMe();
1911 } else if (m_replyButton->defaultAction() == m_replyAll) {
1912 slotReplyAll();
1913 } else if (m_replyButton->defaultAction() == m_replyList) {
1914 slotReplyList();
1915 } else {
1916 slotReplyTo();
1920 void MainWindow::slotForwardAsAttachment()
1922 m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT);
1925 void MainWindow::slotResend()
1927 QModelIndex index;
1928 Imap::Mailbox::Model::realTreeItem(msgListWidget->tree->currentIndex(), nullptr, &index);
1929 if (!index.isValid())
1930 return;
1932 auto recipients = QList<QPair<Composer::RecipientKind,QString>>();
1933 for (const auto &kind: {Imap::Mailbox::RoleMessageTo, Imap::Mailbox::RoleMessageCc, Imap::Mailbox::RoleMessageBcc}) {
1934 for (const auto &oneAddr : index.data(kind).toList()) {
1935 Q_ASSERT(oneAddr.type() == QVariant::StringList);
1936 QStringList item = oneAddr.toStringList();
1937 Q_ASSERT(item.size() == 4);
1938 Imap::Message::MailAddress a(item[0], item[1], item[2], item[3]);
1939 Composer::RecipientKind translatedKind = Composer::RecipientKind::ADDRESS_TO;
1940 switch (kind) {
1941 case Imap::Mailbox::RoleMessageTo:
1942 translatedKind = Composer::RecipientKind::ADDRESS_RESENT_TO;
1943 break;
1944 case Imap::Mailbox::RoleMessageCc:
1945 translatedKind = Composer::RecipientKind::ADDRESS_RESENT_CC;
1946 break;
1947 case Imap::Mailbox::RoleMessageBcc:
1948 translatedKind = Composer::RecipientKind::ADDRESS_RESENT_BCC;
1949 break;
1950 default:
1951 Q_ASSERT(false);
1952 break;
1954 recipients.push_back({translatedKind, a.asPrettyString()});
1958 ComposeWidget::warnIfMsaNotConfigured(
1959 ComposeWidget::createFromReadOnly(this, index, recipients),
1960 this);
1964 void MainWindow::slotComposeMailUrl(const QUrl &url)
1966 ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this);
1969 void MainWindow::slotManageContact(const QUrl &url)
1971 Imap::Message::MailAddress addr;
1972 if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact")))
1973 return;
1975 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1976 if (!addressbook)
1977 return;
1979 addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name);
1982 void MainWindow::invokeContactEditor()
1984 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
1985 if (!addressbook)
1986 return;
1988 addressbook->openAddressbookWindow();
1991 /** @short Create an MSAFactory as per the settings */
1992 MSA::MSAFactory *MainWindow::msaFactory()
1994 using namespace Common;
1995 QString method = m_settings->value(SettingsNames::msaMethodKey).toString();
1996 MSA::MSAFactory *msaFactory = 0;
1997 if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) {
1998 msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(),
1999 m_settings->value(SettingsNames::smtpPortKey).toInt(),
2000 (method == SettingsNames::methodSSMTP),
2001 (method == SettingsNames::methodSMTP)
2002 && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(),
2003 m_settings->value(SettingsNames::smtpAuthKey).toBool(),
2004 m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ?
2005 m_settings->value(SettingsNames::imapUserKey).toString() :
2006 m_settings->value(SettingsNames::smtpUserKey).toString());
2007 } else if (method == SettingsNames::methodSENDMAIL) {
2008 QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' '));
2009 if (args.isEmpty()) {
2010 return 0;
2012 QString appName = args.takeFirst();
2013 msaFactory = new MSA::SendmailFactory(appName, args);
2014 } else if (method == SettingsNames::methodImapSendmail) {
2015 if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) {
2016 return 0;
2018 msaFactory = new MSA::ImapSubmitFactory(qobject_cast<Imap::Mailbox::Model*>(imapAccess()->imapModel()));
2019 } else {
2020 return 0;
2022 return msaFactory;
2025 void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg)
2027 Gui::Util::messageBoxWarning(this, tr("Can't delete mailbox"),
2028 tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
2031 void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg)
2033 Gui::Util::messageBoxWarning(this, tr("Can't create mailbox"),
2034 tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
2037 void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg)
2039 Gui::Util::messageBoxWarning(this, tr("Can't open mailbox"),
2040 tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg));
2043 void MainWindow::slotMailboxChanged(const QModelIndex &mailbox)
2045 using namespace Imap::Mailbox;
2046 QString mailboxName = mailbox.data(RoleMailboxName).toString();
2047 bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() &&
2048 m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() &&
2049 mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString();
2050 QTreeView *tree = msgListWidget->tree;
2052 // Automatically trigger visibility of the TO and FROM columns
2053 if (isSentMailbox) {
2054 if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) {
2055 tree->hideColumn(MsgListModel::FROM);
2056 tree->showColumn(MsgListModel::TO);
2058 } else {
2059 if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) {
2060 tree->hideColumn(MsgListModel::TO);
2061 tree->showColumn(MsgListModel::FROM);
2065 updateMessageFlags();
2066 slotScrollToUnseenMessage();
2069 void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state)
2071 Q_UNUSED(parserId);
2072 static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE;
2073 QString message = connectionStateToString(state);
2075 if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) {
2076 // prevent excessive popups when we "reset the state" to something which is shown quite often
2077 showStatusMessage(QString());
2078 } else {
2079 showStatusMessage(message);
2081 previousState = state;
2084 void MainWindow::slotShowLinkTarget(const QString &link)
2086 if (link.isEmpty()) {
2087 QToolTip::hideText();
2088 } else {
2089 QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link)));
2093 void MainWindow::slotShowAboutTrojita()
2095 Ui::AboutDialog ui;
2096 QDialog *widget = new QDialog(this);
2097 widget->setAttribute(Qt::WA_DeleteOnClose);
2098 ui.setupUi(widget);
2099 ui.versionLabel->setText(Common::Application::version);
2100 ui.qtVersion->setText(QStringLiteral("<a href=\"about-qt\">Qt " QT_VERSION_STR "</a>"));
2101 connect(ui.qtVersion, &QLabel::linkActivated, qApp, &QApplication::aboutQt);
2103 std::vector<std::pair<QString, bool>> features;
2104 features.emplace_back(tr("Plugins"),
2105 #ifdef WITH_SHARED_PLUGINS
2106 true
2107 #else
2108 false
2109 #endif
2111 features.emplace_back(tr("Encrypted and signed messages"),
2112 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
2113 true
2114 #else
2115 false
2116 #endif
2118 features.emplace_back(tr("IMAP compression"),
2119 #ifdef TROJITA_HAVE_ZLIB
2120 true
2121 #else
2122 false
2123 #endif
2126 QString featuresText = QStringLiteral("<ul>");
2127 for (const auto x: features) {
2128 featuresText += x.second ?
2129 tr("<li>%1: supported</li>").arg(x.first)
2130 : tr("<li>%1: <strong>disabled</strong></li>").arg(x.first);
2132 featuresText += QStringLiteral("</ul>");
2133 ui.descriptionLabel->setText(ui.descriptionLabel->text() + featuresText);
2135 QStringList copyright;
2137 // Find the names of the authors and remove date codes from there
2138 QFile license(QStringLiteral(":/LICENSE"));
2139 license.open(QFile::ReadOnly);
2140 const QString prefix(QStringLiteral("Copyright (C) "));
2141 Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) {
2142 if (line.startsWith(prefix)) {
2143 const int pos = prefix.size();
2144 copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') +
2145 line.mid(pos).replace(QRegularExpression(QLatin1String("(\\d) - (\\d)")),
2146 QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2"));
2150 ui.credits->setTextFormat(Qt::PlainText);
2151 ui.credits->setText(copyright.join(QStringLiteral("\n")));
2152 widget->show();
2155 void MainWindow::slotDonateToTrojita()
2157 QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/"));
2160 void MainWindow::slotSaveCurrentMessageBody()
2162 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2163 Q_ASSERT(item.isValid());
2164 if (item.column() != 0)
2165 continue;
2166 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2167 continue;
2169 QModelIndex messageIndex = Imap::deproxifiedIndex(item);
2171 Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this);
2172 netAccess->setModelMessage(messageIndex);
2173 Imap::Network::FileDownloadManager *fileDownloadManager =
2174 new Imap::Network::FileDownloadManager(this, netAccess, messageIndex);
2175 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater);
2176 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater);
2177 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested,
2178 this, &MainWindow::slotDownloadMessageFileNameRequested);
2179 connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError,
2180 this, &MainWindow::slotDownloadTransferError);
2181 connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater);
2182 fileDownloadManager->downloadMessage();
2186 void MainWindow::slotDownloadTransferError(const QString &errorString)
2188 Gui::Util::messageBoxCritical(this, tr("Can't save into file"),
2189 tr("Unable to save into file. Error:\n%1").arg(errorString));
2192 void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName)
2194 *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0,
2195 QFileDialog::HideNameFilterDetails);
2198 void MainWindow::slotViewMsgSource()
2200 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2201 Q_ASSERT(item.isValid());
2202 if (item.column() != 0)
2203 continue;
2204 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2205 continue;
2206 auto w = messageSourceWidget(item);
2207 //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox.
2208 w->setWindowTitle(tr("Message source of UID %1 in %2").arg(
2209 QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()),
2210 Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString()
2212 w->show();
2216 QWidget *MainWindow::messageSourceWidget(const QModelIndex &message)
2218 QModelIndex messageIndex = Imap::deproxifiedIndex(message);
2219 MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex);
2220 sourceWidget->setAttribute(Qt::WA_DeleteOnClose);
2221 QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget);
2222 sourceWidget->addAction(close);
2223 close->setShortcut(tr("Ctrl+W"));
2224 connect(close, &QAction::triggered, sourceWidget, &QWidget::close);
2225 return sourceWidget;
2228 void MainWindow::slotViewMsgHeaders()
2230 Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) {
2231 Q_ASSERT(item.isValid());
2232 if (item.column() != 0)
2233 continue;
2234 if (! item.data(Imap::Mailbox::RoleMessageUid).isValid())
2235 continue;
2236 QModelIndex messageIndex = Imap::deproxifiedIndex(item);
2238 auto widget = new MessageHeadersWidget(nullptr, messageIndex);
2239 widget->setAttribute(Qt::WA_DeleteOnClose);
2240 QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget);
2241 widget->addAction(close);
2242 close->setShortcut(tr("Ctrl+W"));
2243 connect(close, &QAction::triggered, widget, &QWidget::close);
2244 widget->show();
2248 #ifdef XTUPLE_CONNECT
2249 void MainWindow::slotXtSyncCurrentMailbox()
2251 QModelIndex index = mboxTree->currentIndex();
2252 if (! index.isValid())
2253 return;
2255 QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2256 QSettings s;
2257 QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList();
2258 if (xtIncludeMailboxInSync->isChecked()) {
2259 if (! mailboxes.contains(mailbox)) {
2260 mailboxes.append(mailbox);
2262 } else {
2263 mailboxes.removeAll(mailbox);
2265 s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2266 QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes);
2267 prettyMboxModel->xtConnectStatusChanged(index);
2269 #endif
2271 void MainWindow::slotSubscribeCurrentMailbox()
2273 QModelIndex index = mboxTree->currentIndex();
2274 if (! index.isValid())
2275 return;
2277 QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString();
2278 if (m_actionSubscribeMailbox->isChecked()) {
2279 imapModel()->subscribeMailbox(mailbox);
2280 } else {
2281 imapModel()->unsubscribeMailbox(mailbox);
2285 void MainWindow::slotShowOnlySubscribed()
2287 if (m_actionShowOnlySubscribed->isEnabled()) {
2288 m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked());
2289 prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked());
2293 void MainWindow::slotScrollToUnseenMessage()
2295 // Now this is much, much more reliable than messing around with finding out an "interesting message"...
2296 if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) {
2297 // we're using some funky sorting, better don't scroll anywhere
2299 if (m_actionSortDescending->isChecked()) {
2300 msgListWidget->tree->scrollToTop();
2301 } else {
2302 msgListWidget->tree->scrollToBottom();
2306 void MainWindow::slotScrollToCurrent()
2308 // TODO: begs for lambda
2309 if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) {
2310 vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0
2314 void MainWindow::slotThreadMsgList()
2316 // We want to save user's preferences and not override them with "threading disabled" when the server
2317 // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here.
2318 const bool useThreading = actionThreadMsgList->isChecked();
2320 // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make
2321 // sorting rather seldomly used as people shall instead use proper threading.
2322 if (useThreading) {
2323 m_actionSortThreading->setEnabled(true);
2324 if (!m_actionSortThreading->isChecked())
2325 m_actionSortThreading->trigger();
2326 m_actionSortNone->setEnabled(false);
2327 } else {
2328 m_actionSortNone->setEnabled(true);
2329 if (!m_actionSortNone->isChecked())
2330 m_actionSortNone->trigger();
2331 m_actionSortThreading->setEnabled(false);
2334 QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex();
2336 if (useThreading && actionThreadMsgList->isEnabled()) {
2337 msgListWidget->tree->setRootIsDecorated(true);
2338 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true);
2339 } else {
2340 msgListWidget->tree->setRootIsDecorated(false);
2341 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false);
2343 m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading));
2345 if (currentItem.isValid()) {
2346 msgListWidget->tree->scrollTo(currentItem);
2347 } else {
2348 // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view
2349 // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same"
2350 // objects as what was visible before the reshuffling.
2351 slotScrollToUnseenMessage();
2355 void MainWindow::slotSortingPreferenceChanged()
2357 Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
2359 using namespace Imap::Mailbox;
2361 int column = -1;
2362 if (m_actionSortByArrival->isChecked()) {
2363 column = MsgListModel::RECEIVED_DATE;
2364 } else if (m_actionSortByCc->isChecked()) {
2365 column = MsgListModel::CC;
2366 } else if (m_actionSortByDate->isChecked()) {
2367 column = MsgListModel::DATE;
2368 } else if (m_actionSortByFrom->isChecked()) {
2369 column = MsgListModel::FROM;
2370 } else if (m_actionSortBySize->isChecked()) {
2371 column = MsgListModel::SIZE;
2372 } else if (m_actionSortBySubject->isChecked()) {
2373 column = MsgListModel::SUBJECT;
2374 } else if (m_actionSortByTo->isChecked()) {
2375 column = MsgListModel::TO;
2376 } else {
2377 column = -1;
2380 msgListWidget->tree->header()->setSortIndicator(column, order);
2383 void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order)
2385 // don't do anything during initialization
2386 if (!m_actionSortNone)
2387 return;
2389 using namespace Imap::Mailbox;
2390 QAction *action;
2392 switch (column) {
2393 case MsgListModel::SEEN:
2394 case MsgListModel::FLAGGED:
2395 case MsgListModel::ATTACHMENT:
2396 case MsgListModel::COLUMN_COUNT:
2397 case MsgListModel::BCC:
2398 case -1:
2399 if (actionThreadMsgList->isChecked())
2400 action = m_actionSortThreading;
2401 else
2402 action = m_actionSortNone;
2403 break;
2404 case MsgListModel::SUBJECT:
2405 action = m_actionSortBySubject;
2406 break;
2407 case MsgListModel::FROM:
2408 action = m_actionSortByFrom;
2409 break;
2410 case MsgListModel::TO:
2411 action = m_actionSortByTo;
2412 break;
2413 case MsgListModel::CC:
2414 action = m_actionSortByCc;
2415 break;
2416 case MsgListModel::DATE:
2417 action = m_actionSortByDate;
2418 break;
2419 case MsgListModel::RECEIVED_DATE:
2420 action = m_actionSortByArrival;
2421 break;
2422 case MsgListModel::SIZE:
2423 action = m_actionSortBySize;
2424 break;
2425 default:
2426 action = m_actionSortNone;
2429 action->setChecked(true);
2430 if (order == Qt::DescendingOrder)
2431 m_actionSortDescending->setChecked(true);
2432 else
2433 m_actionSortAscending->setChecked(true);
2436 void MainWindow::slotSearchRequested(const QStringList &searchConditions)
2438 Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel =
2439 qobject_cast<Imap::Mailbox::ThreadingMsgListModel *>(m_imapAccess->threadingMsgListModel());
2440 threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(),
2441 threadingMsgListModel->currentSortOrder());
2444 void MainWindow::slotHideRead()
2446 const bool hideRead = actionHideRead->isChecked();
2447 prettyMsgListModel->setHideRead(hideRead);
2448 m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead));
2451 void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities)
2453 msgListWidget->tree->header()->viewport()->removeEventFilter(this);
2454 if (capabilities.contains(QStringLiteral("SORT"))) {
2455 m_actionSortByDate->actionGroup()->setEnabled(true);
2456 } else {
2457 m_actionSortByDate->actionGroup()->setEnabled(false);
2458 msgListWidget->tree->header()->viewport()->installEventFilter(this);
2461 msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY")));
2463 m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED")));
2464 m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() &&
2465 m_settings->value(
2466 Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool());
2467 m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled());
2469 const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities();
2470 Q_FOREACH(const QString &capability, capabilities) {
2471 if (supportedCapabilities.contains(capability)) {
2472 actionThreadMsgList->setEnabled(true);
2473 if (actionThreadMsgList->isChecked())
2474 slotThreadMsgList();
2475 return;
2478 actionThreadMsgList->setEnabled(false);
2481 void MainWindow::slotShowImapInfo()
2483 QString caps;
2484 Q_FOREACH(const QString &cap, imapModel()->capabilities()) {
2485 caps += tr("<li>%1</li>\n").arg(cap);
2488 QString idString;
2489 if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) {
2490 QMap<QByteArray,QByteArray> serverId = imapModel()->serverId();
2492 #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \
2493 QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown");
2494 IMAP_ID_FIELD(serverName, "name");
2495 IMAP_ID_FIELD(serverVersion, "version");
2496 IMAP_ID_FIELD(os, "os");
2497 IMAP_ID_FIELD(osVersion, "os-version");
2498 IMAP_ID_FIELD(vendor, "vendor");
2499 IMAP_ID_FIELD(supportUrl, "support-url");
2500 IMAP_ID_FIELD(address, "address");
2501 IMAP_ID_FIELD(date, "date");
2502 IMAP_ID_FIELD(command, "command");
2503 IMAP_ID_FIELD(arguments, "arguments");
2504 IMAP_ID_FIELD(environment, "environment");
2505 #undef IMAP_ID_FIELD
2506 if (has_serverName) {
2507 idString = tr("<p>");
2508 if (has_serverVersion)
2509 idString += tr("Server: %1 %2").arg(serverName, serverVersion);
2510 else
2511 idString += tr("Server: %1").arg(serverName);
2513 if (has_vendor) {
2514 idString += tr(" (%1)").arg(vendor);
2516 if (has_os) {
2517 if (has_osVersion)
2518 idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion);
2519 else
2520 idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os);
2522 idString += tr("</p>");
2523 } else {
2524 idString = tr("<p>The IMAP server did not return usable information about itself.</p>");
2526 QString fullId;
2527 for (QMap<QByteArray,QByteArray>::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) {
2528 fullId += tr("<li>%1: %2</li>").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped());
2530 idString += tr("<ul>%1</ul>").arg(fullId);
2531 } else {
2532 idString = tr("<p>The server has not provided information about its software version.</p>");
2535 Ui::ImapInfoDialog ui;
2536 QScopedPointer<QDialog> dialog(new QDialog(this));
2538 ui.setupUi(dialog.data());
2539 ui.version->setText(tr("%1\n"
2540 "<p>The following capabilities are currently advertised:</p>").arg(idString));
2541 ui.capabilities->setText(tr("<ul>\n%1</ul>").arg(caps));
2543 dialog->exec();
2546 QSize MainWindow::sizeHint() const
2548 return QSize(1150, 980);
2551 void MainWindow::slotUpdateWindowTitle()
2553 QModelIndex mailbox = qobject_cast<Imap::Mailbox::MsgListModel *>(m_imapAccess->msgListModel())->currentMailbox();
2554 QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
2555 if (!profileName.isEmpty())
2556 profileName = QLatin1String(" [") + profileName + QLatin1Char(']');
2557 if (mailbox.isValid()) {
2558 if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) {
2559 setWindowTitle(tr("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt())
2560 .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2561 } else {
2562 setWindowTitle(tr("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName);
2564 } else {
2565 setWindowTitle(tr("Trojitá") + profileName);
2569 void MainWindow::slotLayoutCompact()
2571 saveSizesAndState();
2572 if (!m_mainHSplitter) {
2573 m_mainHSplitter = new QSplitter();
2574 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2575 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2577 if (!m_mainVSplitter) {
2578 m_mainVSplitter = new QSplitter();
2579 m_mainVSplitter->setOrientation(Qt::Vertical);
2580 connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2581 connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2584 m_mainVSplitter->addWidget(msgListWidget);
2585 m_mainVSplitter->addWidget(m_messageWidget);
2586 m_mainHSplitter->addWidget(mboxTree);
2587 m_mainHSplitter->addWidget(m_mainVSplitter);
2589 mboxTree->show();
2590 msgListWidget->show();
2591 m_messageWidget->show();
2592 m_mainVSplitter->show();
2593 m_mainHSplitter->show();
2595 // The mboxTree shall not expand...
2596 m_mainHSplitter->setStretchFactor(0, 0);
2597 // ...while the msgListTree shall consume all the remaining space
2598 m_mainHSplitter->setStretchFactor(1, 1);
2599 // The CompleteMessageWidget shall not not collapse
2600 m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false);
2602 setCentralWidget(m_mainHSplitter);
2604 delete m_mainStack;
2606 m_layoutMode = LAYOUT_COMPACT;
2607 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact);
2608 applySizesAndState();
2611 void MainWindow::slotLayoutWide()
2613 saveSizesAndState();
2614 if (!m_mainHSplitter) {
2615 m_mainHSplitter = new QSplitter();
2616 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast<void (QTimer::*)()>(&QTimer::start));
2617 connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged);
2620 m_mainHSplitter->addWidget(mboxTree);
2621 m_mainHSplitter->addWidget(msgListWidget);
2622 m_mainHSplitter->addWidget(m_messageWidget);
2623 msgListWidget->resize(mboxTree->size());
2624 m_messageWidget->resize(mboxTree->size());
2625 m_mainHSplitter->setStretchFactor(0, 0);
2626 m_mainHSplitter->setStretchFactor(1, 1);
2627 m_mainHSplitter->setStretchFactor(2, 1);
2629 m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false);
2631 mboxTree->show();
2632 msgListWidget->show();
2633 m_messageWidget->show();
2634 m_mainHSplitter->show();
2636 setCentralWidget(m_mainHSplitter);
2638 delete m_mainStack;
2639 delete m_mainVSplitter;
2641 m_layoutMode = LAYOUT_WIDE;
2642 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide);
2643 applySizesAndState();
2646 void MainWindow::slotLayoutOneAtTime()
2648 saveSizesAndState();
2649 if (m_mainStack)
2650 return;
2652 m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack);
2653 setCentralWidget(m_mainStack);
2655 delete m_mainHSplitter;
2656 delete m_mainVSplitter;
2658 m_layoutMode = LAYOUT_ONE_AT_TIME;
2659 m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime);
2660 applySizesAndState();
2663 Imap::Mailbox::Model *MainWindow::imapModel() const
2665 return qobject_cast<Imap::Mailbox::Model *>(m_imapAccess->imapModel());
2668 void MainWindow::desktopGeometryChanged()
2670 m_delayedStateSaving->start();
2673 QString MainWindow::settingsKeyForLayout(const LayoutMode layout)
2675 switch (layout) {
2676 case LAYOUT_COMPACT:
2677 return Common::SettingsNames::guiSizesInMainWinWhenCompact;
2678 case LAYOUT_WIDE:
2679 return Common::SettingsNames::guiSizesInMainWinWhenWide;
2680 case LAYOUT_ONE_AT_TIME:
2681 return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime;
2682 break;
2684 return QString();
2687 void MainWindow::saveSizesAndState()
2689 if (m_skipSavingOfUI)
2690 return;
2692 QRect geometry = qApp->desktop()->availableGeometry(this);
2693 QString key = settingsKeyForLayout(m_layoutMode);
2694 if (key.isEmpty())
2695 return;
2697 QList<QByteArray> items;
2698 items << saveGeometry();
2699 items << saveState();
2700 items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray());
2701 items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray());
2702 items << msgListWidget->tree->header()->saveState();
2703 items << QByteArray::number(msgListWidget->tree->header()->count());
2704 for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) {
2705 items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i));
2707 // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number
2708 items << QByteArray::number(menuBar()->isVisible());
2709 QByteArray buf;
2710 QDataStream stream(&buf, QIODevice::WriteOnly);
2711 stream << items.size();
2712 Q_FOREACH(const QByteArray &item, items) {
2713 stream << item;
2716 m_settings->setValue(key.arg(QString::number(geometry.width())), buf);
2719 void MainWindow::saveRawStateSetting(bool enabled)
2721 m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled);
2724 void MainWindow::applySizesAndState()
2726 QRect geometry = qApp->desktop()->availableGeometry(this);
2727 QString key = settingsKeyForLayout(m_layoutMode);
2728 if (key.isEmpty())
2729 return;
2731 QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray();
2732 if (buf.isEmpty())
2733 return;
2735 int size;
2736 QDataStream stream(&buf, QIODevice::ReadOnly);
2737 stream >> size;
2738 QByteArray item;
2740 // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to
2741 // save our geometries and state, which is what we must avoid while this method is executing.
2742 bool skipSaving = m_skipSavingOfUI;
2743 m_skipSavingOfUI = true;
2745 if (size-- && !stream.atEnd()) {
2746 stream >> item;
2748 // https://bugreports.qt-project.org/browse/QTBUG-30636
2749 if (windowState() & Qt::WindowMaximized) {
2750 // restoreGeometry(.) restores the wrong size for at least maximized window
2751 // However, QWidget does also not notice that the configure request for this
2752 // is ignored by many window managers (because users really don't like when windows
2753 // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from
2754 // the wrong assumption the request would have been hononred.
2755 // So we just "fix" the internal geometry immediately afterwards to prevent
2756 // mislayouting
2757 // There's atm. no flicker due to this (and because Qt compresses events)
2758 // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true)
2759 QRect oldGeometry = MainWindow::geometry();
2760 restoreGeometry(item);
2761 if (windowState() & Qt::WindowMaximized)
2762 setGeometry(oldGeometry);
2763 } else {
2764 restoreGeometry(item);
2765 if (windowState() & Qt::WindowMaximized) {
2766 // ensure to try setting the proper geometry and have the WM constrain us
2767 setGeometry(QApplication::desktop()->availableGeometry());
2772 if (size-- && !stream.atEnd()) {
2773 stream >> item;
2774 restoreState(item);
2777 if (size-- && !stream.atEnd()) {
2778 stream >> item;
2779 if (m_mainVSplitter) {
2780 m_mainVSplitter->restoreState(item);
2784 if (size-- && !stream.atEnd()) {
2785 stream >> item;
2786 if (m_mainHSplitter) {
2787 m_mainHSplitter->restoreState(item);
2791 if (size-- && !stream.atEnd()) {
2792 stream >> item;
2793 msgListWidget->tree->header()->restoreState(item);
2794 // got to manually update the state of the actions which control the visibility state
2795 msgListWidget->tree->updateActionsAfterRestoredState();
2798 connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns);
2800 if (size-- && !stream.atEnd()) {
2801 stream >> item;
2802 bool ok;
2803 int columns = item.toInt(&ok);
2804 if (ok) {
2805 msgListWidget->tree->header()->setStretchLastSection(false);
2806 for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) {
2807 stream >> item;
2808 int sectionSize = item.toInt();
2809 QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i);
2810 if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) {
2811 // fun fact: user cannot resize by mouse when size <= 0
2812 msgListWidget->tree->setColumnWidth(i, sectionSize);
2813 } else {
2814 msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i));
2816 msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode);
2821 if (size-- && !stream.atEnd()) {
2822 stream >> item;
2823 bool ok;
2824 bool visibility = item.toInt(&ok);
2825 if (ok) {
2826 menuBar()->setVisible(visibility);
2827 showMenuBar->setChecked(visibility);
2831 m_skipSavingOfUI = skipSaving;
2834 void MainWindow::resizeEvent(QResizeEvent *)
2836 m_delayedStateSaving->start();
2839 /** @short Make sure that the message gets loaded after the splitters have changed their position */
2840 void MainWindow::possiblyLoadMessageOnSplittersChanged()
2842 if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) {
2843 // We do not have to check whether it's a different message; the setMessage() will do this or us
2844 // and there are multiple proxy models involved anyway
2845 QModelIndex index = msgListWidget->tree->currentIndex();
2846 if (index.isValid()) {
2847 // OTOH, setting an invalid QModelIndex would happily assert-fail
2848 m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex());
2853 Imap::ImapAccess *MainWindow::imapAccess() const
2855 return m_imapAccess;
2858 void MainWindow::enableLoggingToDisk()
2860 protocolLogger->slotSetPersistentLogging(true);
2863 void MainWindow::slotPluginsChanged()
2865 Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook();
2866 if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow))
2867 m_actionContactEditor->setEnabled(false);
2868 else
2869 m_actionContactEditor->setEnabled(true);
2872 /** @short Update the default action to make sure that we show a correct status of the network connection */
2873 void MainWindow::updateNetworkIndication()
2875 if (QAction *action = qobject_cast<QAction*>(sender())) {
2876 if (action->isChecked()) {
2877 m_netToolbarDefaultAction->setIcon(action->icon());
2882 void MainWindow::showStatusMessage(const QString &message)
2884 networkIndicator->setToolTip(message);
2885 if (isActiveWindow())
2886 QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message);
2889 void MainWindow::slotMessageModelChanged(QAbstractItemModel *model)
2891 mailMimeTree->setModel(model);
2894 void MainWindow::slotFavoriteTagsChanged()
2896 for (int i = 1; i <= m_favoriteTags->rowCount(); ++i) {
2897 QAction *action = ShortcutHandler::instance()->action(QStringLiteral("action_tag_") + QString::number(i));
2898 if (action)
2899 action->setText(tr("Tag with \"%1\"").arg(m_favoriteTags->tagNameByIndex(i - 1)));
2903 void MainWindow::registerComposeWindow(ComposeWidget* widget)
2905 connect(widget, &ComposeWidget::logged, this, [this](const Common::LogKind kind, const QString& source, const QString& message) {
2906 protocolLogger->log(0, Common::LogMessage(QDateTime::currentDateTime(), kind, source, message, 0));