diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a20962c..acc00656 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,846 +1,847 @@ project(trojita) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) # Qt5's qt5_use_modules need 2.8.9+ # On win32, we need QtMain linking, which means 2.8.11+ # EL7 ships 2.8.11, Debian 8 ships 3.0.2, and Ubuntu 14.04 LTS has 2.8.12 cmake_minimum_required(VERSION 2.8.11) cmake_policy(SET CMP0020 NEW) if(POLICY CMP0054) # Silence warnings in TrojitaOption.cmake; we are fine with only doing the # expansion by hand cmake_policy(SET CMP0054 NEW) endif() if(POLICY CMP0043) # We make use of CMAKE_CXXFLAGS_DEBUG... cmake_policy(SET CMP0043 OLD) endif() if(CMAKE_VERSION VERSION_LESS "3.1") # If you aren't using and old Linux with an old GCC and old CMake, please just upgrade. set(CMAKE_CXX_FLAGS "--std=c++11 ${CMAKE_CXX_FLAGS}") else() set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) endif() # Set a default build type if none was specified. This was shamelessly stolen # from VTK's cmake setup because these guys produce both CMake and a project that # manipulates this variable, and the web is full of posts where people say that # it is apparently evil to just set the build type in a way an earlier version of # this patch did. Oh, and the location of this check/update matters, apparently. # # Yes, this is just plain crazy. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() set(CMAKE_POSITION_INDEPENDENT_CODE ON) include(TrojitaOption) trojita_option(WITH_DESKTOP "Build desktop version" ON) trojita_option(WITH_DBUS "Build with DBus library" AUTO) trojita_option(WITH_RAGEL "Build with Ragel library" AUTO) trojita_option(WITH_ZLIB "Build with zlib library" AUTO) trojita_option(WITH_SHARED_PLUGINS "Enable shared dynamic plugins" ON) trojita_option(BUILD_TESTING "Build tests" ON) trojita_option(WITH_MIMETIC "Build with client-side MIME parsing" AUTO) trojita_option(WITH_GPGMEPP "Use GpgME's native C++ bindings" AUTO) trojita_option(WITH_KF5_GPGMEPP "Use legacy discontinued GpgME++ library from KDE frameworks" AUTO) if(WIN32) trojita_option(WITH_NSIS "Build Windows NSIS installer" AUTO "WITH_DESKTOP") endif() if(UNIX AND NOT APPLE) set(QTKEYCHAIN_DEPENDS ";WITH_DBUS") else() set(QTKEYCHAIN_DEPENDS "") endif() find_package(Qt5Core 5.2 REQUIRED) find_package(Qt5Gui REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5Sql REQUIRED) find_package(Qt5WebKitWidgets REQUIRED) find_package(Qt5Widgets REQUIRED) find_package(Qt5LinguistTools) find_package(Qt5Svg REQUIRED) trojita_find_package(Qt5DBus "" "http://qt-project.org" "Qt5 D-Bus support" "Needed for IPC and for some plugins" WITH_DBUS) trojita_find_package(Qt5Test "" "http://qt-project.org" "Qt5 QTest library" "Needed for automated tests" BUILD_TESTING) if(Qt5LinguistTools_FOUND) find_package(Qt5LinguistForTrojita) endif() trojita_plugin_option(WITH_ABOOKADDRESSBOOK_PLUGIN "Build AbookAddressbook plugin" STATIC) trojita_plugin_option(WITH_CLEARTEXT_PLUGIN "Build Cleartext password plugin" STATIC) trojita_plugin_option(WITH_QTKEYCHAIN_PLUGIN "Build Qtkeychain password plugin" "${QTKEYCHAIN_DEPENDS}") trojita_find_package(Git "" "" "" "") trojita_find_package(Mimetic "" "http://www.codesink.org/mimetic_mime_library.html" "C++ MIME Library" "Required for client-side MIME parsing" WITH_MIMETIC) trojita_find_package(Gpgmepp "1.8.0" "https://gnupg.org/related_software/gpgme/index.html" "C++/Qt bindings for gpgme" "Needed for encrypted/signed e-mails" WITH_GPGMEPP) if(NOT WITH_GPGMEPP) trojita_find_package(KF5Gpgmepp "" "https://commits.kde.org/gpgmepp?path=/" "C++ bindings for gpgme" "Needed for encrypted/signed e-mails" WITH_KF5_GPGMEPP) trojita_option(WITH_CRYPTO_MESSAGES "Enable support for encrypted messages" AUTO "WITH_MIMETIC;WITH_KF5_GPGMEPP") else() trojita_option(WITH_CRYPTO_MESSAGES "Enable support for encrypted messages" AUTO "WITH_MIMETIC;WITH_GPGMEPP") endif() if(WIN32) trojita_find_package(MakeNSIS "" "http://nsis.sourceforge.net" "Nullsoft Scriptable Install System" "Needed for building Windows installer" WITH_NSIS) endif() # Add support for Mingw RC compiler if(WIN32) enable_language(RC) include(CMakeDetermineRCCompiler) if(MINGW) set(CMAKE_RC_COMPILER_INIT windres) set(CMAKE_RC_COMPILE_OBJECT " -O coff -I${CMAKE_CURRENT_BINARY_DIR} -i -o ") endif() endif() trojita_find_package(Qt5Keychain QUIET "https://github.com/frankosterfeld/qtkeychain" "QtKeychain library (Qt5 version)" "Needed for QtKeychain password plugin" WITH_QTKEYCHAIN_PLUGIN) if(Qt5Keychain_FOUND OR QtKeychain_FOUND) message(STATUS "Found QtKeychain library (includes at ${QTKEYCHAIN_INCLUDE_DIRS}, lib at ${QTKEYCHAIN_LIBRARIES})") else() message(STATUS "Could not find QtKeychain library") endif() if(NOT DEFINED CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR "lib${LIB_SUFFIX}") endif() mark_as_advanced(CMAKE_INSTALL_LIBDIR) if(NOT CMAKE_INSTALL_PLUGIN_DIR) set(CMAKE_INSTALL_PLUGIN_DIR "${CMAKE_INSTALL_LIBDIR}/trojita") endif() mark_as_advanced(CMAKE_INSTALL_PLUGIN_DIR) if(NOT PLUGIN_DIR) if(IS_ABSOLUTE ${CMAKE_INSTALL_PLUGIN_DIR}) set(PLUGIN_DIR "${CMAKE_INSTALL_PLUGIN_DIR}") else() set(PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_PLUGIN_DIR}") endif() endif() mark_as_advanced(PLUGIN_DIR) include(GNUInstallDirs) # When manipulating CXXFLAGS, we put the user's CXXFLAGS *after* that so that they take priority. if(MSVC) # See below for some reationale for these optimizations set(CMAKE_CXX_FLAGS "/O2 ${CMAKE_CXX_FLAGS}") # We have no information about the warnings and their usefullness. Reports are welcome. # We might enable warnings on MSVC in future. else() # -Werror is not a default for sanity reasons (one cannot know what warnings a future compiler # might bring along), but it's a default in debug mode. The idea is that developers should care # about a warning-free build, and that this is easier than messing with yet another configure option. set(CMAKE_CXX_FLAGS_DEBUG "-Werror ${CMAKE_CXX_FLAGS_DEBUG}") # Also see CMP0043... # Optimizations are enabled unconditionally because they make a big difference in the speed of the # resulting binaries, and that it is better to allow an opt-out from them by adjusting CXXFLAGS through # an env var at cmake time if needed. # The reason for not manipulating just CMAKE_CXX_FLAGS_DEBUG is that unrecognized build types ("DebugFull") # should still benefit from these optimizations. Yup, it would be even better if CMake did a sane thing # and warned when users set an unrecognized and unused build type, but that just isn't the case. set(CMAKE_CXX_FLAGS "-O2 ${CMAKE_CXX_FLAGS}") # Build warnings are useful tools (and Trojita should be warning-free anyway), enable them on all # configurations. They are warnings, not errors. set(CMAKE_CXX_FLAGS "-Wall -Wsign-compare ${CMAKE_CXX_FLAGS}") endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) # The following is required so that the moc_*.cpp and ui_*.h are found include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_definitions(-DQT_STRICT_ITERATORS) add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_USE_FAST_OPERATOR_PLUS) add_definitions(-DQT_USE_FAST_CONCATENATION) if(NOT MSVC) # We're using C++11's threading features (std::async in particular), and that requires "some threading". With GCC and # Clang, this is implemented through the -pthread build flag. Without using these bits, linking fails on Fedora 23, # and this is apparently a slightly different failure than the Kf5::Gpgmepp-pthread one in commit # 12e41101070f7073caec653185c0504763672ee7. # # Apparently, there's been various methods on how to enable this in the most-cmakeish-way throughout the years, with # cmake-3.1+ supporting some magic linking via the Threads::Threads option on a per-library basis. However, I am not # really looking into that wonderful fun of mixing -pthread and non-pthread translation units, so let's use a big # hammer and set it unconditionally on platforms which use it anyway. # # And because we also support MinGW and its `windres` compiler, we have to avoid passing -pthread to *that* thing, so, # well, let's cheat and put it into the CXXFLAGS. set(CMAKE_CXX_FLAGS "-pthread ${CMAKE_CXX_FLAGS}") endif() # Make sure that plugins not export all symbols, only that which are explicitly marked include(GenerateExportHeader) add_compiler_export_flags() set(CMAKE_AUTOMOC True) trojita_find_package(RagelForTrojita "" "" "" "" WITH_RAGEL) trojita_find_package(ZLIB "" "" "" "" WITH_ZLIB) if(WITH_MIMETIC) set(TROJITA_HAVE_MIMETIC True) else() set(TROJITA_HAVE_MIMETIC False) endif() if(WITH_GPGMEPP OR WITH_KF5_GPGMEPP) set(TROJITA_HAVE_GPGMEPP True) else() set(TROJITA_HAVE_GPGMEPP False) endif() if(WITH_CRYPTO_MESSAGES) set(TROJITA_HAVE_CRYPTO_MESSAGES True) else() set(TROJITA_HAVE_CRYPTO_MESSAGES False) endif() if(WITH_ZLIB) set(TROJITA_HAVE_ZLIB True) message(STATUS "Support for COMPRESS=DEFLATE enabled") else() set(TROJITA_HAVE_ZLIB False) message(STATUS "Disabling COMPRESS=DEFLATE, zlib is not available") endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/configure.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/configure.cmake.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/configure-plugins.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/configure-plugins.cmake.h) feature_summary(FATAL_ON_MISSING_REQUIRED_PACKAGES INCLUDE_QUIET_PACKAGES DESCRIPTION "\n" WHAT ALL) set(path_Common ${CMAKE_CURRENT_SOURCE_DIR}/src/Common) set(libCommon_SOURCES ${path_Common}/Application.cpp ${path_Common}/ConnectionId.cpp ${path_Common}/FileLogger.cpp ${path_Common}/MetaTypes.cpp ${path_Common}/Paths.cpp ${path_Common}/SettingsNames.cpp ${path_Common}/StashingReverseIterator.h ) set(path_Plugins ${CMAKE_CURRENT_SOURCE_DIR}/src/Plugins) set(libPlugins_SOURCES ${path_Plugins}/AddressbookPlugin.cpp ${path_Plugins}/PasswordPlugin.cpp ${path_Plugins}/PluginJob.cpp ${path_Plugins}/PluginManager.cpp ) set(path_UiUtils ${CMAKE_CURRENT_SOURCE_DIR}/src/UiUtils) set(libUiUtils_SOURCES ${path_UiUtils}/Color.cpp ${path_UiUtils}/Formatting.cpp ${path_UiUtils}/IconLoader.cpp ${path_UiUtils}/PartLoadingOptions.h ${path_UiUtils}/PartVisitor.h ${path_UiUtils}/PartWalker.h ${path_UiUtils}/PartWalker_impl.h ${path_UiUtils}/PasswordWatcher.cpp ${path_UiUtils}/PlainTextFormatter.cpp ${path_UiUtils}/QaimDfsIterator.cpp ) set(path_IPC ${CMAKE_CURRENT_SOURCE_DIR}/src/IPC) if(WITH_DBUS) set(libIPC_SOURCES ${path_IPC}/DBusInterface.cpp ${path_IPC}/MainWindowBridge.cpp ) else() set(libIPC_SOURCES ${path_IPC}/None.cpp ) endif() set(path_Composer ${CMAKE_CURRENT_SOURCE_DIR}/src/Composer) set(libComposer_SOURCES ${path_Composer}/AbstractComposer.cpp ${path_Composer}/ComposerAttachments.cpp ${path_Composer}/ExistingMessageComposer.cpp ${path_Composer}/Mailto.cpp ${path_Composer}/MessageComposer.cpp ${path_Composer}/QuoteText.cpp ${path_Composer}/Recipients.cpp ${path_Composer}/ReplaceSignature.cpp ${path_Composer}/SenderIdentitiesModel.cpp ${path_Composer}/SubjectMangling.cpp ${path_Composer}/Submission.cpp ) set(path_MSA ${CMAKE_CURRENT_SOURCE_DIR}/src/MSA) set(libMSA_SOURCES ${path_MSA}/AbstractMSA.cpp ${path_MSA}/Account.cpp ${path_MSA}/FakeMSA.cpp ${path_MSA}/ImapSubmit.cpp ${path_MSA}/SMTP.cpp ${path_MSA}/Sendmail.cpp ) set(path_Streams ${CMAKE_CURRENT_SOURCE_DIR}/src/Streams) set(libStreams_SOURCES ${path_Streams}/DeletionWatcher.cpp ${path_Streams}/FakeSocket.cpp ${path_Streams}/IODeviceSocket.cpp ${path_Streams}/Socket.cpp ${path_Streams}/SocketFactory.cpp ) set(path_Cryptography ${CMAKE_CURRENT_SOURCE_DIR}/src/Cryptography) set(libCryptography_SOURCES ${path_Cryptography}/MessageModel.cpp ${path_Cryptography}/MessagePart.cpp ${path_Cryptography}/PartReplacer.cpp ) if(WITH_MIMETIC) set(libCryptography_SOURCES ${libCryptography_SOURCES} ${path_Cryptography}/LocalMimeParser.cpp ${path_Cryptography}/MimeticUtils.cpp ) endif() if(WITH_CRYPTO_MESSAGES) set(libCryptography_SOURCES ${libCryptography_SOURCES} ${path_Cryptography}/GpgMe++.cpp ) endif() if(WITH_ZLIB) set(libStreams_SOURCES ${libStreams_SOURCES} ${path_Streams}/3rdparty/rfc1951.cpp) endif() set(path_DesktopGui ${CMAKE_CURRENT_SOURCE_DIR}/src/Gui) set(libDesktopGui_SOURCES ${path_DesktopGui}/AddressRowWidget.cpp ${path_DesktopGui}/AttachmentView.cpp ${path_DesktopGui}/ColoredItemDelegate.cpp ${path_DesktopGui}/CompleteMessageWidget.cpp ${path_DesktopGui}/ComposeWidget.cpp ${path_DesktopGui}/ComposerAttachmentsList.cpp ${path_DesktopGui}/ComposerTextEdit.cpp ${path_DesktopGui}/EmbeddedWebView.cpp ${path_DesktopGui}/EnvelopeView.cpp ${path_DesktopGui}/ExternalElementsWidget.cpp ${path_DesktopGui}/FindBar.cpp ${path_DesktopGui}/FindBarMixin.cpp ${path_DesktopGui}/FlowLayout.cpp ${path_DesktopGui}/FromAddressProxyModel.cpp ${path_DesktopGui}/LineEdit.cpp ${path_DesktopGui}/LoadablePartWidget.cpp ${path_DesktopGui}/MailBoxTreeView.cpp ${path_DesktopGui}/MessageHeadersWidget.cpp ${path_DesktopGui}/MessageListWidget.cpp ${path_DesktopGui}/MessageSourceWidget.cpp ${path_DesktopGui}/MessageView.cpp + ${path_DesktopGui}/MsgItemDelegate.cpp ${path_DesktopGui}/MsgListView.cpp ${path_DesktopGui}/OnePanelAtTimeWidget.cpp ${path_DesktopGui}/OneEnvelopeAddress.cpp ${path_DesktopGui}/OverlayWidget.cpp ${path_DesktopGui}/PartWalker.cpp ${path_DesktopGui}/PartWidget.cpp ${path_DesktopGui}/PartWidgetFactoryVisitor.cpp ${path_DesktopGui}/PasswordDialog.cpp ${path_DesktopGui}/ProgressPopUp.cpp ${path_DesktopGui}/ProtocolLoggerWidget.cpp ${path_DesktopGui}/ReplaceCharValidator.cpp ${path_DesktopGui}/SettingsDialog.cpp ${path_DesktopGui}/SimplePartWidget.cpp ${path_DesktopGui}/Spinner.cpp ${path_DesktopGui}/TagAddDialog.cpp ${path_DesktopGui}/TagListWidget.cpp ${path_DesktopGui}/TagWidget.cpp ${path_DesktopGui}/TaskProgressIndicator.cpp ${path_DesktopGui}/UserAgentWebPage.cpp ${path_DesktopGui}/Util.cpp ${path_DesktopGui}/Window.cpp ${path_DesktopGui}/ShortcutHandler/ShortcutConfigDialog.cpp ${path_DesktopGui}/ShortcutHandler/ShortcutConfigWidget.cpp ${path_DesktopGui}/ShortcutHandler/ShortcutHandler.cpp ) set(libDesktopGui_UI ${path_DesktopGui}/AboutDialog.ui ${path_DesktopGui}/ComposeWidget.ui ${path_DesktopGui}/CreateMailboxDialog.ui ${path_DesktopGui}/EditIdentity.ui ${path_DesktopGui}/EditFavoriteTag.ui ${path_DesktopGui}/PasswordDialog.ui ${path_DesktopGui}/ProgressPopUp.ui ${path_DesktopGui}/SettingsCachePage.ui ${path_DesktopGui}/SettingsGeneralPage.ui ${path_DesktopGui}/SettingsImapPage.ui ${path_DesktopGui}/SettingsOutgoingPage.ui ${path_DesktopGui}/SettingsFavoriteTagsPage.ui ${path_DesktopGui}/TagAddDialog.ui ${path_DesktopGui}/ShortcutHandler/ShortcutConfigWidget.ui ) set(libDesktopGui_RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/icons.qrc ${CMAKE_CURRENT_SOURCE_DIR}/src/license.qrc ) set(libqwwsmtpclient_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/qwwsmtpclient/qwwsmtpclient.cpp) set(libAppVersion_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/AppVersion/SetCoreApplication.cpp) set(path_Imap ${CMAKE_CURRENT_SOURCE_DIR}/src/Imap) set(libImap_SOURCES ${path_Imap}/ConnectionState.cpp ${path_Imap}/Encoders.cpp ${path_Imap}/Exceptions.cpp ${path_Imap}/Parser/3rdparty/kcodecs.cpp ${path_Imap}/Parser/3rdparty/rfccodecs.cpp ${path_Imap}/Parser/Command.cpp ${path_Imap}/Parser/Data.cpp ${path_Imap}/Parser/LowLevelParser.cpp ${path_Imap}/Parser/MailAddress.cpp ${path_Imap}/Parser/Message.cpp ${path_Imap}/Parser/Parser.cpp ${path_Imap}/Parser/Response.cpp ${path_Imap}/Parser/Sequence.cpp ${path_Imap}/Parser/ThreadingNode.cpp ${path_Imap}/Network/FileDownloadManager.cpp ${path_Imap}/Network/ForbiddenReply.cpp ${path_Imap}/Network/MsgPartNetAccessManager.cpp ${path_Imap}/Network/MsgPartNetworkReply.cpp ${path_Imap}/Network/QQuickNetworkReplyWrapper.cpp ${path_Imap}/Model/Cache.cpp ${path_Imap}/Model/CombinedCache.cpp ${path_Imap}/Model/DragAndDrop.cpp ${path_Imap}/Model/DiskPartCache.cpp ${path_Imap}/Model/DummyNetworkWatcher.cpp ${path_Imap}/Model/FindInterestingPart.cpp ${path_Imap}/Model/FlagsOperation.cpp ${path_Imap}/Model/FullMessageCombiner.cpp ${path_Imap}/Model/ImapAccess.cpp ${path_Imap}/Model/MailboxFinder.cpp ${path_Imap}/Model/MailboxMetadata.cpp ${path_Imap}/Model/MailboxModel.cpp ${path_Imap}/Model/MailboxTree.cpp ${path_Imap}/Model/MemoryCache.cpp ${path_Imap}/Model/Model.cpp ${path_Imap}/Model/MsgListModel.cpp ${path_Imap}/Model/NetworkWatcher.cpp ${path_Imap}/Model/OneMessageModel.cpp ${path_Imap}/Model/FavoriteTagsModel.cpp ${path_Imap}/Model/ParserState.cpp ${path_Imap}/Model/PrettyMailboxModel.cpp ${path_Imap}/Model/PrettyMsgListModel.cpp ${path_Imap}/Model/SpecialFlagNames.cpp ${path_Imap}/Model/SQLCache.cpp ${path_Imap}/Model/SubtreeModel.cpp ${path_Imap}/Model/SystemNetworkWatcher.cpp ${path_Imap}/Model/TaskFactory.cpp ${path_Imap}/Model/TaskPresentationModel.cpp ${path_Imap}/Model/ThreadingMsgListModel.cpp ${path_Imap}/Model/Utils.cpp ${path_Imap}/Model/VisibleTasksModel.cpp # The ModelWatcher is another debugging aid ${path_Imap}/Model/ModelWatcher.cpp ${path_Imap}/Model/kdeui-itemviews/kdescendantsproxymodel.cpp ${path_Imap}/Tasks/AppendTask.cpp ${path_Imap}/Tasks/CopyMoveMessagesTask.cpp ${path_Imap}/Tasks/CreateMailboxTask.cpp ${path_Imap}/Tasks/DeleteMailboxTask.cpp ${path_Imap}/Tasks/EnableTask.cpp ${path_Imap}/Tasks/ExpungeMailboxTask.cpp ${path_Imap}/Tasks/ExpungeMessagesTask.cpp ${path_Imap}/Tasks/Fake_ListChildMailboxesTask.cpp ${path_Imap}/Tasks/Fake_OpenConnectionTask.cpp ${path_Imap}/Tasks/FetchMsgMetadataTask.cpp ${path_Imap}/Tasks/FetchMsgPartTask.cpp ${path_Imap}/Tasks/GenUrlAuthTask.cpp ${path_Imap}/Tasks/GetAnyConnectionTask.cpp ${path_Imap}/Tasks/IdTask.cpp ${path_Imap}/Tasks/IdleLauncher.cpp ${path_Imap}/Tasks/ImapTask.cpp ${path_Imap}/Tasks/KeepMailboxOpenTask.cpp ${path_Imap}/Tasks/ListChildMailboxesTask.cpp ${path_Imap}/Tasks/NoopTask.cpp ${path_Imap}/Tasks/NumberOfMessagesTask.cpp ${path_Imap}/Tasks/ObtainSynchronizedMailboxTask.cpp ${path_Imap}/Tasks/OfflineConnectionTask.cpp ${path_Imap}/Tasks/OpenConnectionTask.cpp ${path_Imap}/Tasks/SortTask.cpp ${path_Imap}/Tasks/SubscribeUnsubscribeTask.cpp ${path_Imap}/Tasks/ThreadTask.cpp ${path_Imap}/Tasks/UidSubmitTask.cpp ${path_Imap}/Tasks/UnSelectTask.cpp ${path_Imap}/Tasks/UpdateFlagsTask.cpp ${path_Imap}/Tasks/UpdateFlagsOfAllMessagesTask.cpp ) # The ModelTest is only needed when debugging manually if (CMAKE_BUILD_TYPE STREQUAL "Debug") list(APPEND libImap_SOURCES ${path_Imap}/Model/ModelTest/modeltest.cpp) endif() if(WITH_RAGEL) message(STATUS "Using Ragel for the RFC 5322 parser") ragel_parser(${path_Imap}/Parser/Rfc5322HeaderParser.cpp) set(libImap_SOURCES ${libImap_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/Rfc5322HeaderParser.generated.cpp) else() message(STATUS "Using pregenerated RFC 5322 parser") set(libImap_SOURCES ${libImap_SOURCES} ${path_Imap}/Parser/Rfc5322HeaderParser.generated.cpp) endif() set(trojita_desktop_SOURCES ${path_DesktopGui}/main.cpp ) if(WIN32) list(APPEND trojita_desktop_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/trojita_win32.rc) set_property(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/trojita_win32.rc APPEND PROPERTY OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/icons/trojita.ico ${CMAKE_CURRENT_BINARY_DIR}/trojita-git-version.h ${CMAKE_CURRENT_BINARY_DIR}/trojita-version.h ) endif() if(Qt5LinguistForTrojita_FOUND) file(GLOB_RECURSE lang_PO "${CMAKE_CURRENT_SOURCE_DIR}/po/trojita_common_*.po") qt5_wrap_po(trojita_QM ${lang_PO}) set(language_summary "") foreach(po ${lang_PO}) string(REGEX REPLACE "^(.*)/trojita_common_(.*).po" "\\2" lang ${po}) list(APPEND language_summary ${lang}) endforeach() list(SORT language_summary) list(LENGTH language_summary num_languages) if(num_languages) message(STATUS "Available languages: ${language_summary}") if(WITH_DESKTOP) install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/locale/ DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/trojita/locale" REGEX "(x_test)|(.*\\.ts)" EXCLUDE) endif() else() message(STATUS "No .po files found, will not install any languages") endif() else() message(STATUS "Qt Linguist (lupdate/lrelease/lconvert) not found, disabling localization support") endif() set(version_files ${CMAKE_CURRENT_BINARY_DIR}/trojita-version.h ${CMAKE_CURRENT_BINARY_DIR}/trojita-git-version.h) if(WITH_NSIS) set(version_files ${version_files} ${CMAKE_CURRENT_BINARY_DIR}/trojita-version.nsi) set(NSIS TRUE) endif() add_custom_target(version DEPENDS version_fake_file) add_custom_command(OUTPUT version_fake_file ${version_files} COMMAND ${CMAKE_COMMAND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DNSIS=${NSIS} -DHOST_ARCH=${HOST_ARCH} -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/TrojitaVersion.cmake) set_source_files_properties(${version_files} PROPERTIES GENERATED TRUE HEADER_FILE_ONLY TRUE) add_library(Common STATIC ${libCommon_SOURCES}) set_property(TARGET Common APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) add_dependencies(Common version) target_link_libraries(Common Qt5::Network) add_library(AppVersion STATIC ${libAppVersion_SOURCES}) set_property(TARGET AppVersion APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) add_dependencies(AppVersion version) target_link_libraries(AppVersion Common) target_link_libraries(AppVersion Qt5::Core) if(WITH_SHARED_PLUGINS) add_library(Plugins SHARED ${libPlugins_SOURCES}) set_property(TARGET Plugins APPEND PROPERTY COMPILE_DEFINITIONS BUILDING_LIBTROJITA_PLUGINS) else() add_library(Plugins STATIC ${libPlugins_SOURCES}) set_property(TARGET Plugins APPEND PROPERTY COMPILE_DEFINITIONS QT_STATICPLUGIN) endif() set_target_properties(Plugins PROPERTIES OUTPUT_NAME trojita_plugins) set_property(TARGET Plugins APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(Plugins Qt5::Core) add_library(UiUtils STATIC ${libUiUtils_SOURCES}) set_property(TARGET UiUtils APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(UiUtils Plugins Common Qt5::Gui) add_library(Streams STATIC ${libStreams_SOURCES}) set_property(TARGET Streams APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) if(WITH_ZLIB) set_property(TARGET Streams APPEND PROPERTY INCLUDE_DIRECTORIES ${ZLIB_INCLUDE_DIR}) target_link_libraries(Streams ${ZLIB_LIBRARIES}) endif() target_link_libraries(Streams Qt5::Network) add_library(IPC STATIC ${libIPC_SOURCES}) set_property(TARGET IPC APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) if(WITH_DBUS) target_link_libraries(IPC Qt5::DBus Qt5::Widgets) else() target_link_libraries(IPC Qt5::Core) endif() add_library(qwwsmtpclient STATIC ${libqwwsmtpclient_SOURCES}) target_link_libraries(qwwsmtpclient Qt5::Network) add_library(MSA STATIC ${libMSA_SOURCES}) set_property(TARGET MSA APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(MSA Imap Streams qwwsmtpclient) add_library(Composer STATIC ${libComposer_SOURCES}) set_property(TARGET Composer APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(Composer Common MSA Streams UiUtils qwwsmtpclient) add_library(Imap STATIC ${libImap_SOURCES}) set_property(TARGET Imap APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(Imap Common Streams UiUtils Qt5::Sql) add_library(Cryptography STATIC ${libCryptography_SOURCES}) set_property(TARGET Cryptography APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(Cryptography Common Imap) if(WITH_MIMETIC) target_link_libraries(Cryptography ${MIMETIC_LIBRARIES}) set_property(TARGET Cryptography APPEND PROPERTY INCLUDE_DIRECTORIES ${MIMETIC_INCLUDE_DIRS}) endif() if(WITH_CRYPTO_MESSAGES) if(WITH_GPGMEPP) target_link_libraries(Cryptography Gpgmepp QGpgme) elseif(WITH_KF5_GPGMEPP) if(WIN32) target_link_libraries(Cryptography KF5::Gpgmepp KF5::QGpgme) else() target_link_libraries(Cryptography KF5::Gpgmepp-pthread KF5::QGpgme) endif() endif() endif() ## ClearText password plugin if(WITH_CLEARTEXT_PLUGIN) trojita_add_plugin(trojita_plugin_ClearTextPasswordPlugin WITH_CLEARTEXT_PLUGIN src/Plugins/ClearTextPassword/ClearTextPassword.cpp) endif() ## QtKeyChain plugin if(WITH_QTKEYCHAIN_PLUGIN) trojita_add_plugin(trojita_plugin_QtKeychainPasswordPlugin WITH_QTKEYCHAIN_PLUGIN src/Plugins/QtKeyChain/QtKeyChainPassword.cpp) target_link_libraries(trojita_plugin_QtKeychainPasswordPlugin ${QTKEYCHAIN_LIBRARIES} Qt5::DBus) set_property(TARGET trojita_plugin_QtKeychainPasswordPlugin APPEND PROPERTY INCLUDE_DIRECTORIES ${QTKEYCHAIN_INCLUDE_DIRS}) endif() ## AbookAddressbook plugin if(WITH_ABOOKADDRESSBOOK_PLUGIN) set(path_AbookAddressbook ${CMAKE_CURRENT_SOURCE_DIR}/src/Plugins/AbookAddressbook) set(libAbookAddressbook_HEADERS ${path_AbookAddressbook}/AbookAddressbook.h ${path_AbookAddressbook}/be-contacts.h ) set(libAbookAddressbook_SOURCES ${path_AbookAddressbook}/AbookAddressbook.cpp ${path_AbookAddressbook}/be-contacts.cpp ) set(libAbookAddressbook_UI ${path_AbookAddressbook}/be-contacts.ui ${path_AbookAddressbook}/onecontact.ui ) qt5_wrap_ui(libAbookAddressbook_UI_OUT ${libAbookAddressbook_UI}) trojita_add_plugin(trojita_plugin_AbookAddressbookPlugin WITH_ABOOKADDRESSBOOK_PLUGIN ${libAbookAddressbook_SOURCES} ${libAbookAddressbook_UI_OUT}) set_property(TARGET trojita_plugin_AbookAddressbookPlugin APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(trojita_plugin_AbookAddressbookPlugin Qt5::Widgets) set(be_contacts_SOURCES ${path_AbookAddressbook}/main.cpp ) add_executable(be.contacts WIN32 ${be_contacts_SOURCES}) set_property(TARGET be.contacts APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(be.contacts Plugins) if("${WITH_ABOOKADDRESSBOOK_PLUGIN}" STREQUAL "STATIC") set_property(TARGET be.contacts APPEND PROPERTY COMPILE_DEFINITIONS QT_STATICPLUGIN) target_link_libraries(be.contacts trojita_plugin_AbookAddressbookPlugin) endif() target_link_libraries(be.contacts Qt5::Widgets) endif() # Generate file static_plugins.h.in get_property(STATIC_PLUGINS GLOBAL PROPERTY TROJITA_STATIC_PLUGINS) file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/static_plugins.h.in "#include \n") foreach(PLUGIN ${STATIC_PLUGINS}) file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/static_plugins.h.in "Q_IMPORT_PLUGIN(${PLUGIN})\n") endforeach() execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/static_plugins.h.in ${CMAKE_CURRENT_BINARY_DIR}/static_plugins.h) if(WITH_DESKTOP) qt5_wrap_ui(libDesktopGui_UI_OUT ${libDesktopGui_UI}) qt5_add_resources(libDesktopGui_RESOURCES_OUT ${libDesktopGui_RESOURCES}) add_library(DesktopGui STATIC ${libDesktopGui_SOURCES} ${libDesktopGui_UI_OUT} ${libDesktopGui_RESOURCES_OUT}) set_property(TARGET DesktopGui APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) # The following is needed for the LineEdit widget within the .ui files. # The ${path_DesktopGui} is needed so that the generated ui_*.h file can find the headers of the custom widgets set_property(TARGET DesktopGui APPEND PROPERTY INCLUDE_DIRECTORIES ${path_DesktopGui}) target_link_libraries(DesktopGui Common UiUtils Composer Cryptography Imap IPC MSA Plugins Streams qwwsmtpclient Qt5::WebKitWidgets) # On Windows build a real Win32 GUI application without console window # On other platforms WIN32 flag is ignored add_executable(trojita WIN32 ${trojita_desktop_SOURCES} ${trojita_QM}) set_property(TARGET trojita APPEND PROPERTY COMPILE_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII) target_link_libraries(trojita AppVersion Common UiUtils DesktopGui ${STATIC_PLUGINS}) endif() if(WITH_SHARED_PLUGINS) install(TARGETS Plugins DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif() include(SanitizedDesktopFile) if(WITH_ABOOKADDRESSBOOK_PLUGIN) install(TARGETS be.contacts RUNTIME DESTINATION bin) endif() if(WITH_DESKTOP) copy_desktop_file_without_cruft("${CMAKE_CURRENT_SOURCE_DIR}/src/Gui/org.kde.trojita.desktop" "${CMAKE_CURRENT_BINARY_DIR}/org.kde.trojita-DesktopGui.desktop") install(TARGETS trojita RUNTIME DESTINATION bin) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.trojita-DesktopGui.desktop DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications/" RENAME org.kde.trojita.desktop) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/Gui/org.kde.trojita.appdata.xml DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo/") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/icons/trojita.png DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps/") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/icons/trojita.svg DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps/") endif() if(WITH_NSIS) include(TrojitaNSIS) endif() if(BUILD_TESTING) set(test_LibMailboxSync_SOURCES tests/Utils/ModelEvents.cpp tests/Utils/LibMailboxSync.cpp ) add_library(test_LibMailboxSync STATIC ${test_LibMailboxSync_SOURCES}) set_property(TARGET test_LibMailboxSync APPEND PROPERTY INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Utils) target_link_libraries(test_LibMailboxSync Imap MSA Streams Common Composer Qt5::Test) macro(trojita_test dir fname) set(test_${fname}_SOURCES tests/${dir}/test_${fname}.cpp) add_executable(test_${fname} ${test_${fname}_SOURCES}) target_link_libraries(test_${fname} Imap MSA Streams Common Composer Cryptography test_LibMailboxSync) set_property(TARGET test_${fname} APPEND PROPERTY INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/tests) if(NOT CMAKE_CROSSCOMPILING) add_test(test_${fname} test_${fname}) endif() endmacro() set(UBSAN_ENV_SUPPRESSIONS "UBSAN_OPTIONS=suppressions=${CMAKE_CURRENT_SOURCE_DIR}/tests/ubsan.supp") enable_testing() trojita_test(Composer Composer_Submission) trojita_test(Composer Composer_Existing) trojita_test(Composer Composer_responses) target_link_libraries(test_Composer_responses Qt5::WebKitWidgets) trojita_test(Composer Html_formatting) target_link_libraries(test_Html_formatting Qt5::WebKitWidgets) trojita_test(Imap Imap_DisappearingMailboxes) trojita_test(Imap Imap_Idle) trojita_test(Imap Imap_LowLevelParser) trojita_test(Imap Imap_Message) trojita_test(Imap Imap_Model) trojita_test(Imap Imap_MsgPartNetAccessManager) set_property(TEST test_Imap_MsgPartNetAccessManager PROPERTY ENVIRONMENT "${UBSAN_ENV_SUPPRESSIONS}") trojita_test(Imap Imap_Parser_parse) trojita_test(Imap Imap_Parser_write) trojita_test(Imap Imap_Responses) trojita_test(Imap Imap_SelectedMailboxUpdates) trojita_test(Imap Imap_Tasks_CreateMailbox) trojita_test(Imap Imap_Tasks_DeleteMailbox) trojita_test(Imap Imap_Tasks_ListChildMailboxes) trojita_test(Imap Imap_Tasks_ObtainSynchronizedMailbox) trojita_test(Imap Imap_Tasks_OpenConnection) trojita_test(Imap Imap_Threading) trojita_test(Imap Imap_BodyParts) trojita_test(Imap Imap_Offline) trojita_test(Imap Imap_CopyAndFlagOperations) trojita_test(Cryptography Cryptography_MessageModel) if(WITH_CRYPTO_MESSAGES) find_program(GPGCONF_BINARY NAMES gpgconf) if(GPGCONF_BINARY_NOTFOUND) message(SEND_ERROR "The `gpgconf` binary from GnuPG not found, this is needed for crypto tests.") endif() if(NOT UNIX) message(SEND_ERROR "The Cryptography unit tests really need Unix. Patches welcome.") endif() add_library(fake-dev-random SHARED ${CMAKE_CURRENT_SOURCE_DIR}/tests/Utils/fake-dev-random.c) set_target_properties(fake-dev-random PROPERTIES AUTOMOC off) target_link_libraries(fake-dev-random dl) # FIXME: it would be nice to depend on the contents of keys/, but in my testing it produces Makefiles # which suffer from races (the keygen.sh is run multiple times in parallel within the CI environment). # I wasn't able to track down the root cause behind this; it affected all cmake versions within the CI # as of Feb 2016, which is 3.1.something up to 3.3.something. add_custom_command(OUTPUT crypto_test_data.h COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/tests/Cryptography/keygen.sh ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS fake-dev-random tests/Cryptography/keygen.sh tests/Cryptography/batch-keygen) add_custom_target(crypto_test_data DEPENDS crypto_test_data.h) trojita_test(Cryptography Cryptography_PGP) set_property(TEST test_Cryptography_PGP PROPERTY ENVIRONMENT "${UBSAN_ENV_SUPPRESSIONS}") add_dependencies(test_Cryptography_PGP crypto_test_data) endif() trojita_test(Misc Rfc5322) trojita_test(Misc RingBuffer) trojita_test(Misc SenderIdentitiesModel) trojita_test(Misc SqlCache) trojita_test(Misc algorithms) trojita_test(Misc rfccodecs) trojita_test(Misc prettySize) trojita_test(Misc Formatting) trojita_test(Misc QaimDfsIterator) trojita_test(Misc FavoriteTagsModel) endif() if(WIN32) # Check if we are on Windows if(MSVC10) # Check if we are using the Visual Studio compiler 2010 # Because of linker errors (see http://stackoverflow.com/questions/5625884/conversion-of-stdwstring-to-qstring-throws-linker-error) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:wchar_t-") elseif(MINGW) else() message(WARNING "You are using a compiler which we have not tested yet (not MSVC10 or MINGW).") message(WARNING "Please let us know how well it works.") endif() endif() diff --git a/src/Gui/ColoredItemDelegate.cpp b/src/Gui/ColoredItemDelegate.cpp index 25cd1c24..4d785ee9 100644 --- a/src/Gui/ColoredItemDelegate.cpp +++ b/src/Gui/ColoredItemDelegate.cpp @@ -1,57 +1,62 @@ /* Copyright (C) Roland Pallai This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ColoredItemDelegate.h" namespace Gui { ColoredItemDelegate::ColoredItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void ColoredItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - QStyleOptionViewItem viewOption(option); QColor itemForegroundColor = index.data(Qt::ForegroundRole).value(); + paintWithForeground(painter, option, index, itemForegroundColor); +} + +void ColoredItemDelegate::paintWithForeground(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QColor &itemForegroundColor) const +{ + QStyleOptionViewItem viewOption(option); if (itemForegroundColor.isValid()) { // Thunderbird works like this and we like it viewOption.palette.setColor(QPalette::ColorGroup::Active, QPalette::Highlight, itemForegroundColor); // We have to make sure that the text remains readable with the new Highlight color. // This covers most of the cases out there if not all. Feel free to extend if you are using a very // special theme. auto fgLightnessF = viewOption.palette.color(QPalette::ColorGroup::Active, QPalette::HighlightedText).lightnessF(); auto bgLightnessF = viewOption.palette.color(QPalette::ColorGroup::Active, QPalette::Highlight).lightnessF(); if (fgLightnessF > 0.75 && bgLightnessF > 0.75) { viewOption.palette.setColor(QPalette::ColorGroup::Active, QPalette::HighlightedText, Qt::black); } else if (fgLightnessF < 0.25 && bgLightnessF < 0.25) { viewOption.palette.setColor(QPalette::ColorGroup::Active, QPalette::HighlightedText, Qt::white); } viewOption.palette.setColor(QPalette::ColorGroup::Inactive, QPalette::HighlightedText, itemForegroundColor); } QStyledItemDelegate::paint(painter, viewOption, index); } } diff --git a/src/Gui/ColoredItemDelegate.h b/src/Gui/ColoredItemDelegate.h index 36a29715..050dd530 100644 --- a/src/Gui/ColoredItemDelegate.h +++ b/src/Gui/ColoredItemDelegate.h @@ -1,47 +1,48 @@ /* Copyright (C) Roland Pallai This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef COLOREDITEMDELEGATE_H #define COLOREDITEMDELEGATE_H #include #include #include #include namespace Gui { /** @short A slightly tweaked QStyledItemDelegate for painting colored items (message, favtag, etc) * * The main role is to propagate text color on highlight. */ class ColoredItemDelegate: public QStyledItemDelegate { Q_OBJECT public: explicit ColoredItemDelegate(QObject* parent); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void paintWithForeground(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QColor &itemForegroundColor) const; }; } #endif /* COLOREDITEMDELEGATE_H */ diff --git a/src/Gui/MessageListWidget.cpp b/src/Gui/MessageListWidget.cpp index 586eb313..05f2215d 100644 --- a/src/Gui/MessageListWidget.cpp +++ b/src/Gui/MessageListWidget.cpp @@ -1,295 +1,295 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MessageListWidget.h" #include #include #include #include #include #include #include #include #include #include "LineEdit.h" #include "MsgListView.h" #include "ReplaceCharValidator.h" #include "UiUtils/IconLoader.h" namespace Gui { -MessageListWidget::MessageListWidget(QWidget *parent) : +MessageListWidget::MessageListWidget(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel) : QWidget(parent), m_supportsFuzzySearch(false) { - tree = new MsgListView(this); + tree = new MsgListView(this, m_favoriteTagsModel); m_quickSearchText = new LineEdit(this); m_quickSearchText->setHistoryEnabled(true); // Filter out newline. It will wreak havoc into the direct IMAP passthrough and could lead to data loss. QValidator *validator = new ReplaceCharValidator(QLatin1Char('\n'), QLatin1Char(' '), m_quickSearchText); m_quickSearchText->setValidator(validator); m_quickSearchText->setPlaceholderText(tr("Quick Search")); m_quickSearchText->setToolTip(tr("Type in a text to search for within this mailbox. " "The icon on the left can be used to limit the search options " "(like whether to include addresses or message bodies, etc)." "

" "Experts who have read RFC3501 can use the := prefix and switch to a raw IMAP mode.")); m_queryPlaceholder = tr(""); connect(m_quickSearchText, &QLineEdit::returnPressed, this, &MessageListWidget::slotApplySearch); connect(m_quickSearchText, &QLineEdit::textChanged, this, &MessageListWidget::slotConditionalSearchReset); connect(m_quickSearchText, &QLineEdit::cursorPositionChanged, this, &MessageListWidget::slotUpdateSearchCursor); m_searchOptions = new QAction(UiUtils::loadIcon(QStringLiteral("imap-search-details")), QStringLiteral("*"), this); m_searchOptions->setToolTip(tr("Options for the IMAP search...")); QMenu *optionsMenu = new QMenu(this); m_searchOptions->setMenu(optionsMenu); m_searchFuzzy = optionsMenu->addAction(tr("Fuzzy Search")); m_searchFuzzy->setCheckable(true); optionsMenu->addSeparator(); m_searchInSubject = optionsMenu->addAction(tr("Subject")); m_searchInSubject->setCheckable(true); m_searchInSubject->setChecked(true); m_searchInBody = optionsMenu->addAction(tr("Body")); m_searchInBody->setCheckable(true); m_searchInSenders = optionsMenu->addAction(tr("Senders")); m_searchInSenders->setCheckable(true); m_searchInSenders->setChecked(true); m_searchInRecipients = optionsMenu->addAction(tr("Recipients")); m_searchInRecipients->setCheckable(true); optionsMenu->addSeparator(); QMenu *complexMenu = new QMenu(tr("Complex IMAP query"), optionsMenu); connect(complexMenu, &QMenu::triggered, this, &MessageListWidget::slotComplexSearchInput); complexMenu->addAction(tr("Not ..."))->setData(QString(QLatin1String("NOT ") + m_queryPlaceholder)); complexMenu->addAction(tr("Either... or..."))->setData(QString(QLatin1String("OR ") + m_queryPlaceholder + QLatin1Char(' ') + m_queryPlaceholder)); complexMenu->addSeparator(); complexMenu->addAction(tr("From sender"))->setData(QString(QLatin1String("FROM ") + m_queryPlaceholder)); complexMenu->addAction(tr("To receiver"))->setData(QString(QLatin1String("TO ") + m_queryPlaceholder)); complexMenu->addSeparator(); complexMenu->addAction(tr("About subject"))->setData(QString(QLatin1String("SUBJECT " )+ m_queryPlaceholder)); complexMenu->addAction(tr("Message contains ..."))->setData(QString(QLatin1String("BODY ") + m_queryPlaceholder)); complexMenu->addSeparator(); complexMenu->addAction(tr("Before date"))->setData(QLatin1String("BEFORE ")); complexMenu->addAction(tr("Since date"))->setData(QLatin1String("SINCE ")); complexMenu->addSeparator(); complexMenu->addAction(tr("Has been seen"))->setData(QLatin1String("SEEN")); m_rawSearch = optionsMenu->addAction(tr("Allow raw IMAP search")); m_rawSearch->setCheckable(true); QAction *rawSearchMenu = optionsMenu->addMenu(complexMenu); rawSearchMenu->setVisible(false); connect(m_rawSearch, &QAction::toggled, rawSearchMenu, &QAction::setVisible); connect(m_rawSearch, &QAction::toggled, this, &MessageListWidget::rawSearchSettingChanged); m_searchOptions->setMenu(optionsMenu); connect(optionsMenu, &QMenu::aboutToShow, this, &MessageListWidget::slotDeActivateSimpleSearch); m_quickSearchText->addAction(m_searchOptions, QLineEdit::LeadingPosition); connect(m_searchOptions, &QAction::triggered, optionsMenu, [this, optionsMenu](){ optionsMenu->popup(m_quickSearchText->mapToGlobal(QPoint(0, m_quickSearchText->height())), nullptr); }); QVBoxLayout *layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_quickSearchText); layout->addWidget(tree); m_searchResetTimer = new QTimer(this); m_searchResetTimer->setSingleShot(true); connect(m_searchResetTimer, &QTimer::timeout, this, &MessageListWidget::slotApplySearch); slotAutoEnableDisableSearch(); } void MessageListWidget::focusSearch() { if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus()) return; m_quickSearchText->setFocus(Qt::ShortcutFocusReason); } void MessageListWidget::focusRawSearch() { if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus() || !m_rawSearch->isChecked()) return; m_quickSearchText->setFocus(Qt::ShortcutFocusReason); m_quickSearchText->setText(QStringLiteral(":=")); m_quickSearchText->deselect(); m_quickSearchText->setCursorPosition(m_quickSearchText->text().length()); } void MessageListWidget::slotApplySearch() { emit requestingSearch(searchConditions()); } void MessageListWidget::slotAutoEnableDisableSearch() { bool isEnabled; if (!m_quickSearchText->text().isEmpty()) { // Some search criteria are in effect and suddenly all matching messages // disappear. We have to make sure that the search bar remains enabled. isEnabled = true; } else if (tree && tree->model()) { isEnabled = tree->model()->rowCount(); } else { isEnabled = false; } m_quickSearchText->setEnabled(isEnabled); m_searchOptions->setEnabled(isEnabled); } void MessageListWidget::slotSortingFailed() { QPalette pal = m_quickSearchText->palette(); pal.setColor(m_quickSearchText->backgroundRole(), Qt::red); pal.setColor(m_quickSearchText->foregroundRole(), Qt::white); m_quickSearchText->setPalette(pal); QTimer::singleShot(500, this, SLOT(slotResetSortingFailed())); } void MessageListWidget::slotResetSortingFailed() { m_quickSearchText->setPalette(QPalette()); } void MessageListWidget::slotConditionalSearchReset() { if (m_quickSearchText->text().isEmpty()) m_searchResetTimer->start(250); else m_searchResetTimer->stop(); } void MessageListWidget::slotUpdateSearchCursor() { int cp = m_quickSearchText->cursorPosition(); int ts = -1, te = -1; for (int i = cp-1; i > -1; --i) { if (m_quickSearchText->text().at(i) == QLatin1Char('>')) break; // invalid if (m_quickSearchText->text().at(i) == QLatin1Char('<')) { ts = i; break; // found TagStart } } if (ts < 0) return; // not inside tag! for (int i = cp; i < m_quickSearchText->text().length(); ++i) { if (m_quickSearchText->text().at(i) == QLatin1Char('<')) break; // invalid if (m_quickSearchText->text().at(i) == QLatin1Char('>')) { te = i; break; // found TagEnd } } if (te < 0) return; // not inside tag? if (m_quickSearchText->text().midRef(ts, m_queryPlaceholder.length()) == m_queryPlaceholder) m_quickSearchText->setSelection(ts, m_queryPlaceholder.length()); } void MessageListWidget::slotComplexSearchInput(QAction *act) { QString s = act->data().toString(); const int selectionStart = m_quickSearchText->selectionStart() - 1; if (selectionStart > -1 && m_quickSearchText->text().at(selectionStart) != QLatin1Char(' ')) s.prepend(QLatin1Char(' ')); m_quickSearchText->insert(s); if (!m_quickSearchText->text().startsWith(QLatin1String(":="))) { s = m_quickSearchText->text().trimmed(); m_quickSearchText->setText(QLatin1String(":=") + s); } m_quickSearchText->setFocus(); const int pos = m_quickSearchText->text().indexOf(m_queryPlaceholder); if (pos > -1) m_quickSearchText->setSelection(pos, m_queryPlaceholder.length()); } void MessageListWidget::slotDeActivateSimpleSearch() { const bool isEnabled = !(m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(QLatin1String(":="))); m_searchInSubject->setEnabled(isEnabled); m_searchInBody->setEnabled(isEnabled); m_searchInSenders->setEnabled(isEnabled); m_searchInRecipients->setEnabled(isEnabled); m_searchFuzzy->setEnabled(isEnabled && m_supportsFuzzySearch); } QStringList MessageListWidget::searchConditions() const { if (!m_quickSearchText->isEnabled() || m_quickSearchText->text().isEmpty()) return QStringList(); static QString rawPrefix = QStringLiteral(":="); if (m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(rawPrefix)) { // It's a "raw" IMAP search, let's simply pass it through return QStringList() << m_quickSearchText->text().mid(rawPrefix.size()); } QStringList keys; if (m_searchInSubject->isChecked()) keys << QStringLiteral("SUBJECT"); if (m_searchInBody->isChecked()) keys << QStringLiteral("BODY"); if (m_searchInRecipients->isChecked()) keys << QStringLiteral("TO") << QStringLiteral("CC") << QStringLiteral("BCC"); if (m_searchInSenders->isChecked()) keys << QStringLiteral("FROM"); if (keys.isEmpty()) return keys; QStringList res; Q_FOREACH(const QString &key, keys) { if (m_supportsFuzzySearch) res << QStringLiteral("FUZZY"); res << key << m_quickSearchText->text(); } if (keys.size() > 1) { // Got to make this a conjunction. The OR operator's reverse-polish-notation accepts just two operands, though. int num = keys.size() - 1; for (int i = 0; i < num; ++i) { res.prepend(QStringLiteral("OR")); } } return res; } void MessageListWidget::setFuzzySearchSupported(bool supported) { m_supportsFuzzySearch = supported; m_searchFuzzy->setEnabled(supported); m_searchFuzzy->setChecked(supported); } void MessageListWidget::setRawSearchEnabled(bool enabled) { m_rawSearch->setChecked(enabled); } } diff --git a/src/Gui/MessageListWidget.h b/src/Gui/MessageListWidget.h index 93323297..24c79f9d 100644 --- a/src/Gui/MessageListWidget.h +++ b/src/Gui/MessageListWidget.h @@ -1,89 +1,90 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MESSAGELISTWIDGET_H #define MESSAGELISTWIDGET_H #include +#include "Imap/Model/FavoriteTagsModel.h" class LineEdit; class QTimer; class QToolButton; namespace Gui { class MsgListView; /** @short Widget containing a list of messages along with quick filtering toolbar */ class MessageListWidget : public QWidget { Q_OBJECT public: - explicit MessageListWidget(QWidget *parent = 0); + explicit MessageListWidget(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel); void setFuzzySearchSupported(bool supported); void setRawSearchEnabled(bool enabled); QStringList searchConditions() const; // FIXME: consider making this private and moving the logic from Window with it MsgListView *tree; public slots: void focusSearch(); void focusRawSearch(); signals: void requestingSearch(const QStringList &conditions); void rawSearchSettingChanged(bool enabled); protected slots: void slotApplySearch(); void slotAutoEnableDisableSearch(); void slotSortingFailed(); private slots: void slotComplexSearchInput(QAction*); void slotConditionalSearchReset(); void slotDeActivateSimpleSearch(); void slotResetSortingFailed(); void slotUpdateSearchCursor(); private: LineEdit *m_quickSearchText; QAction *m_searchOptions; QAction *m_searchInSubject; QAction *m_searchInBody; QAction *m_searchInSenders; QAction *m_searchInRecipients; QAction *m_searchFuzzy; QAction *m_rawSearch; bool m_supportsFuzzySearch; QTimer *m_searchResetTimer; QString m_queryPlaceholder; friend class MainWindow; // needs access to our private slots }; } #endif // MESSAGELISTWIDGET_H diff --git a/src/Gui/MsgItemDelegate.cpp b/src/Gui/MsgItemDelegate.cpp new file mode 100644 index 00000000..e509615e --- /dev/null +++ b/src/Gui/MsgItemDelegate.cpp @@ -0,0 +1,62 @@ +/* Copyright (C) Roland Pallai + + This file is part of the Trojita Qt IMAP e-mail client, + http://trojita.flaska.net/ + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "MsgItemDelegate.h" +#include +#include "Imap/Model/ItemRoles.h" +#include "Imap/Model/MsgListModel.h" +#include "MsgListView.h" + +namespace Gui +{ + +MsgItemDelegate::MsgItemDelegate(QObject *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel) : + ColoredItemDelegate(parent), m_favoriteTagsModel(m_favoriteTagsModel) +{ +} + +QColor MsgItemDelegate::itemColor(const QModelIndex &index) const +{ + auto view = static_cast(parent()); + + Q_ASSERT(index.isValid()); + QModelIndex sindex = index.sibling(index.row(), Imap::Mailbox::MsgListModel::SUBJECT); + QVariant flags; + if (!sindex.model()->hasChildren(sindex) || view->isExpanded(sindex)) { + flags = index.data(Imap::Mailbox::RoleMessageFlags); + } else { + flags = index.data(Imap::Mailbox::RoleThreadAggregatedFlags); + } + + return m_favoriteTagsModel->findBestColorForTags(flags.toStringList()); +} + +void MsgItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItem viewOption(option); + auto foregroundColor = itemColor(index); + if (foregroundColor.isValid()) + viewOption.palette.setColor(QPalette::Text, foregroundColor); + ColoredItemDelegate::paintWithForeground(painter, viewOption, index, foregroundColor); +} + +} diff --git a/src/Gui/ColoredItemDelegate.h b/src/Gui/MsgItemDelegate.h similarity index 71% copy from src/Gui/ColoredItemDelegate.h copy to src/Gui/MsgItemDelegate.h index 36a29715..4bb522e0 100644 --- a/src/Gui/ColoredItemDelegate.h +++ b/src/Gui/MsgItemDelegate.h @@ -1,47 +1,50 @@ /* Copyright (C) Roland Pallai This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#ifndef COLOREDITEMDELEGATE_H -#define COLOREDITEMDELEGATE_H +#ifndef MSGITEMDELEGATE_H +#define MSGITEMDELEGATE_H #include #include -#include #include +#include "Imap/Model/FavoriteTagsModel.h" +#include "ColoredItemDelegate.h" namespace Gui { -/** @short A slightly tweaked QStyledItemDelegate for painting colored items (message, favtag, etc) - * - * The main role is to propagate text color on highlight. - */ -class ColoredItemDelegate: public QStyledItemDelegate +/** @short Painting colorized messages in MsgListView +*/ +class MsgItemDelegate : public ColoredItemDelegate { Q_OBJECT public: - explicit ColoredItemDelegate(QObject* parent); + explicit MsgItemDelegate(QObject* parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; +private: + QColor itemColor(const QModelIndex &index) const; + + Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel; }; } -#endif /* COLOREDITEMDELEGATE_H */ +#endif /* MSGITEMDELEGATE_H */ diff --git a/src/Gui/MsgListView.cpp b/src/Gui/MsgListView.cpp index ed5c1b44..d7f5f036 100644 --- a/src/Gui/MsgListView.cpp +++ b/src/Gui/MsgListView.cpp @@ -1,506 +1,507 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MsgListView.h" #include #include #include #include #include #include #include #include #include #include -#include "ColoredItemDelegate.h" +#include "MsgItemDelegate.h" #include "Imap/Model/MsgListModel.h" #include "Imap/Model/PrettyMsgListModel.h" #include "Imap/Model/ThreadingMsgListModel.h" namespace Gui { -MsgListView::MsgListView(QWidget *parent): QTreeView(parent), m_autoActivateAfterKeyNavigation(true), m_autoResizeSections(true) +MsgListView::MsgListView(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel): + QTreeView(parent), m_autoActivateAfterKeyNavigation(true), m_autoResizeSections(true) { connect(header(), &QHeaderView::geometriesChanged, this, &MsgListView::slotFixSize); connect(this, &QTreeView::expanded, this, &MsgListView::slotExpandWholeSubtree); connect(header(), &QHeaderView::sectionCountChanged, this, &MsgListView::slotUpdateHeaderActions); header()->setContextMenuPolicy(Qt::ActionsContextMenu); headerFieldsMapper = new QSignalMapper(this); connect(headerFieldsMapper, static_cast(&QSignalMapper::mapped), this, &MsgListView::slotHeaderSectionVisibilityToggled); setUniformRowHeights(true); setAllColumnsShowFocus(true); setSelectionMode(ExtendedSelection); setDragEnabled(true); setRootIsDecorated(false); // Some subthreads might be huuuuuuuuuuge, so prevent indenting them too heavily setIndentation(15); - setItemDelegate(new ColoredItemDelegate(this)); + setItemDelegate(new MsgItemDelegate(this, m_favoriteTagsModel)); setSortingEnabled(true); // By default, we don't do any sorting header()->setSortIndicator(-1, Qt::AscendingOrder); m_naviActivationTimer = new QTimer(this); m_naviActivationTimer->setSingleShot(true); connect(m_naviActivationTimer, &QTimer::timeout, this, &MsgListView::slotCurrentActivated); } // left might collapse a thread, question is whether ending there (on closing the thread) should be // taken as mail loading request (i don't think so, but it's sth. that needs to be figured over time) // NOTICE: reasonably Triggers should be a (non strict) subset of Blockers (user changed his mind) // the list of key events which pot. lead to loading a new message. static QList gs_naviActivationTriggers = QList() << Qt::Key_Up << Qt::Key_Down << Qt::Key_Right << Qt::Key_Left << Qt::Key_PageUp << Qt::Key_PageDown << Qt::Key_Home << Qt::Key_End; // the list of key events which cancel naviActivationTrigger induced action. static QList gs_naviActivationBlockers = QList() << Qt::Key_Up << Qt::Key_Down << Qt::Key_Left << Qt::Key_PageUp << Qt::Key_PageDown << Qt::Key_Home << Qt::Key_End; void MsgListView::keyPressEvent(QKeyEvent *ke) { if (gs_naviActivationBlockers.contains(ke->key())) m_naviActivationTimer->stop(); QTreeView::keyPressEvent(ke); } void MsgListView::keyReleaseEvent(QKeyEvent *ke) { if (ke->modifiers() == Qt::NoModifier && gs_naviActivationTriggers.contains(ke->key())) m_naviActivationTimer->start(150); // few ms for the user to re-orientate. 150ms is not much QTreeView::keyReleaseEvent(ke); } bool MsgListView::event(QEvent *event) { if (event->type() == QEvent::ShortcutOverride && !gs_naviActivationBlockers.contains(static_cast(event)->key()) && m_naviActivationTimer->isActive()) { // Make sure that the delayed timer is broken ASAP when the key looks like something which might possibly be a shortcut m_naviActivationTimer->stop(); slotCurrentActivated(); } return QTreeView::event(event); } void MsgListView::slotCurrentActivated() { if (currentIndex().isValid() && m_autoActivateAfterKeyNavigation) { // The "current index" is the one with that funny dot which only triggers the read/unread status toggle. // If we don't do anything, subsequent pressing of key_up or key_down will move the cursor up/down one row // while preserving the column which will lead to toggling the read/unread state of *that* message. // That's unexpected; the key shall just move the cursor and change the current message. emit activated(currentIndex().sibling(currentIndex().row(), Imap::Mailbox::MsgListModel::SUBJECT)); } } int MsgListView::sizeHintForColumn(int column) const { QFont boldFont = font(); boldFont.setBold(true); QFontMetrics metric(boldFont); switch (column) { case Imap::Mailbox::MsgListModel::SEEN: return 0; case Imap::Mailbox::MsgListModel::FLAGGED: case Imap::Mailbox::MsgListModel::ATTACHMENT: return style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, nullptr); case Imap::Mailbox::MsgListModel::SUBJECT: return metric.size(Qt::TextSingleLine, QStringLiteral("Blesmrt Trojita Foo Bar Random Text")).width(); case Imap::Mailbox::MsgListModel::FROM: case Imap::Mailbox::MsgListModel::TO: case Imap::Mailbox::MsgListModel::CC: case Imap::Mailbox::MsgListModel::BCC: return metric.size(Qt::TextSingleLine, QStringLiteral("Blesmrt Trojita")).width(); case Imap::Mailbox::MsgListModel::DATE: case Imap::Mailbox::MsgListModel::RECEIVED_DATE: return metric.size(Qt::TextSingleLine, //: Translators: use a text which is returned for e-mails older than one day but newer than one week //: (see UiUtils::Formatting::prettyDate() for the string formats); the idea here //: is to have a text which is "wide enough" in a typical UI font. //: The English version uses "Mon" because of the M letter; you should use something similar. tr("Mon 10")).width(); case Imap::Mailbox::MsgListModel::SIZE: return metric.size(Qt::TextSingleLine, tr("88.8 kB")).width(); default: return QTreeView::sizeHintForColumn(column); } } /** @short Reimplemented to show custom pixmap during drag&drop Qt's model-view classes don't provide any means of interfering with the QDrag's pixmap so we just rip off QAbstractItemView::startDrag and provide our own QPixmap. */ void MsgListView::startDrag(Qt::DropActions supportedActions) { // indexes for column 0, i.e. subject QModelIndexList baseIndexes; Q_FOREACH(const QModelIndex &index, selectedIndexes()) { if (!(model()->flags(index) & Qt::ItemIsDragEnabled)) continue; if (index.column() == Imap::Mailbox::MsgListModel::SUBJECT) baseIndexes << index; } if (!baseIndexes.isEmpty()) { QMimeData *data = model()->mimeData(baseIndexes); if (!data) return; // use screen width and itemDelegate()->sizeHint() to determine size of the pixmap int screenWidth = QApplication::desktop()->screenGeometry(this).width(); int maxWidth = qMax(400, screenWidth / 4); QSize size(maxWidth, 0); // Show a "+ X more items" text after so many entries const int maxItems = 20; QStyleOptionViewItem opt; opt.initFrom(this); opt.rect.setWidth(maxWidth); opt.rect.setHeight(itemDelegate()->sizeHint(opt, baseIndexes.at(0)).height()); size.setHeight(qMin(maxItems + 1, baseIndexes.size()) * opt.rect.height()); // State_Selected provides for nice background of the items opt.state |= QStyle::State_Selected; // paint list of selected items using itemDelegate() to be consistent with style QPixmap pixmap(size); pixmap.fill(Qt::transparent); QPainter p(&pixmap); for (int i = 0; i < baseIndexes.size(); ++i) { opt.rect.moveTop(i * opt.rect.height()); if (i == maxItems) { p.fillRect(opt.rect, palette().color(QPalette::Disabled, QPalette::Highlight)); p.setBrush(palette().color(QPalette::Disabled, QPalette::HighlightedText)); p.drawText(opt.rect, Qt::AlignRight, tr("+ %n additional item(s)", 0, baseIndexes.size() - maxItems)); break; } itemDelegate()->paint(&p, opt, baseIndexes.at(i)); } QDrag *drag = new QDrag(this); drag->setPixmap(pixmap); drag->setMimeData(data); drag->setHotSpot(QPoint(0, 0)); Qt::DropAction dropAction = Qt::IgnoreAction; if (defaultDropAction() != Qt::IgnoreAction && (supportedActions & defaultDropAction())) dropAction = defaultDropAction(); else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove) dropAction = Qt::CopyAction; if (drag->exec(supportedActions, dropAction) == Qt::MoveAction) { // QAbstractItemView::startDrag calls d->clearOrRemove() here, so // this is a copy of QAbstractItemModelPrivate::clearOrRemove(); const QItemSelection selection = selectionModel()->selection(); QList::const_iterator it = selection.constBegin(); if (!dragDropOverwriteMode()) { for (; it != selection.constEnd(); ++it) { QModelIndex parent = it->parent(); if (it->left() != 0) continue; if (it->right() != (model()->columnCount(parent) - 1)) continue; int count = it->bottom() - it->top() + 1; model()->removeRows(it->top(), count, parent); } } else { // we can't remove the rows so reset the items (i.e. the view is like a table) QModelIndexList list = selection.indexes(); for (int i = 0; i < list.size(); ++i) { QModelIndex index = list.at(i); QMap roles = model()->itemData(index); for (QMap::Iterator it = roles.begin(); it != roles.end(); ++it) it.value() = QVariant(); model()->setItemData(index, roles); } } } } } void MsgListView::slotFixSize() { if (!m_autoResizeSections) return; if (header()->visualIndex(Imap::Mailbox::MsgListModel::SUBJECT) == -1) { // calling setResizeMode() would assert() return; } header()->setStretchLastSection(false); for (int i = 0; i < Imap::Mailbox::MsgListModel::COLUMN_COUNT; ++i) { QHeaderView::ResizeMode resizeMode = resizeModeForColumn(i); header()->setSectionResizeMode(i, resizeMode); setColumnWidth(i, sizeHintForColumn(i)); } } QHeaderView::ResizeMode MsgListView::resizeModeForColumn(const int column) const { switch (column) { case Imap::Mailbox::MsgListModel::SUBJECT: return QHeaderView::Stretch; case Imap::Mailbox::MsgListModel::SEEN: case Imap::Mailbox::MsgListModel::FLAGGED: case Imap::Mailbox::MsgListModel::ATTACHMENT: return QHeaderView::Fixed; default: return QHeaderView::Interactive; } } void MsgListView::slotExpandWholeSubtree(const QModelIndex &rootIndex) { if (rootIndex.parent().isValid()) return; QVector queue(1, rootIndex); for (int i = 0; i < queue.size(); ++i) { const QModelIndex currentIndex = queue[i]; // Append all children to the queue... for (int j = 0; j < currentIndex.model()->rowCount(currentIndex); ++j) queue.append(currentIndex.child(j, 0)); // ...and expand the current index if (currentIndex.model()->hasChildren(currentIndex)) expand(currentIndex); } } void MsgListView::slotUpdateHeaderActions() { Q_ASSERT(header()); // At first, remove all actions QList actions = header()->actions(); Q_FOREACH(QAction *action, actions) { header()->removeAction(action); headerFieldsMapper->removeMappings(action); action->deleteLater(); } actions.clear(); // Now add them again for (int i = 0; i < header()->count(); ++i) { QString message = header()->model() ? header()->model()->headerData(i, Qt::Horizontal).toString() : QString::number(i); QAction *action = new QAction(message, this); action->setCheckable(true); action->setChecked(true); connect(action, &QAction::toggled, headerFieldsMapper, static_cast(&QSignalMapper::map)); headerFieldsMapper->setMapping(action, i); header()->addAction(action); // Next, add some special handling of certain columns switch (i) { case Imap::Mailbox::MsgListModel::SEEN: // This column doesn't have a textual description action->setText(tr("Seen status")); break; case Imap::Mailbox::MsgListModel::FLAGGED: action->setText(tr("Flagged status")); break; case Imap::Mailbox::MsgListModel::ATTACHMENT: action->setText(tr("Attachment")); break; case Imap::Mailbox::MsgListModel::TO: case Imap::Mailbox::MsgListModel::CC: case Imap::Mailbox::MsgListModel::BCC: case Imap::Mailbox::MsgListModel::RECEIVED_DATE: // And these should be hidden by default action->toggle(); break; default: break; } } // Make sure to kick the header again so that it shows reasonable sizing slotFixSize(); } /** @short Handle columns added to MsgListModel and set their default properties * * When a new version of the underlying model got a new column, the old saved state of the GUI might only contain data for the old columns. * Therefore it is important to explicitly restore the default for new columns, if any. */ void MsgListView::slotHandleNewColumns(int oldCount, int newCount) { for (int i = oldCount; i < newCount; ++i) { switch(i) { case Imap::Mailbox::MsgListModel::FLAGGED: header()->moveSection(i,0); break; case Imap::Mailbox::MsgListModel::ATTACHMENT: header()->moveSection(i,0); break; } setColumnWidth(i, sizeHintForColumn(i)); } } void MsgListView::slotHeaderSectionVisibilityToggled(int section) { QList actions = header()->actions(); if (section >= actions.size() || section < 0) return; bool hide = ! actions[section]->isChecked(); if (hide && header()->hiddenSectionCount() == header()->count() - 1) { // This would hide the very last section, which would hide the whole header view actions[section]->setChecked(true); } else { header()->setSectionHidden(section, hide); } } void MsgListView::updateActionsAfterRestoredState() { m_autoResizeSections = false; QList actions = header()->actions(); for (int i = 0; i < actions.size(); ++i) { actions[i]->setChecked(!header()->isSectionHidden(i)); } } /** @short Overridden from QTreeView::setModel The whole point is that we have to listen for sortingPreferenceChanged to update your header view when sorting is requested but cannot be fulfilled. */ void MsgListView::setModel(QAbstractItemModel *model) { if (this->model()) { if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(this->model())) { disconnect(prettyModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MsgListView::slotHandleSortCriteriaChanged); disconnect(qobject_cast(prettyModel->sourceModel())->sourceModel(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &MsgListView::slotMsgListModelRowsAboutToBeRemoved); } } QTreeView::setModel(model); if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(model)) { connect(prettyModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MsgListView::slotHandleSortCriteriaChanged); connect(qobject_cast(prettyModel->sourceModel())->sourceModel(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &MsgListView::slotMsgListModelRowsAboutToBeRemoved); } } /** @short Get ThreadingMsgListModel index and call the next handler */ void MsgListView::slotMsgListModelRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); auto threadingModel = qobject_cast(findPrettyMsgListModel(model())->sourceModel()); for (int i = start; i <= end; ++i) { QModelIndex index = threadingModel->sourceModel()->index(i, 0, parent); Q_ASSERT(index.isValid()); QModelIndex translated = threadingModel->mapFromSource(index); if (translated.isValid()) slotThreadingMsgListModelRowAboutToBeRemoved(translated); } } /** @short Keep the cursor in place for better keyboard usability In the worst case this is an O(log n). Such a worst case is when messages are removed in descending order starting from last one of the view. But in practice, there are many cases when it performs well better. A performance-wise approach could be to hook signal layoutAboutToBeChanged, but the underlying model has removed the rows by then and it makes everything complicated. */ void MsgListView::slotThreadingMsgListModelRowAboutToBeRemoved(const QModelIndex &index) { Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(model()); Q_ASSERT(!index.isValid() || index.model() == qobject_cast(prettyModel->sourceModel())); QModelIndex current = currentIndex(); if (current.isValid() && prettyModel->mapFromSource(index) == current) { setCurrentIndexToNextValid(current); } } /** @short Try to move the cursor to next message Used when the current message disappearing. */ void MsgListView::setCurrentIndexToNextValid(const QModelIndex ¤t) { Q_ASSERT(current.isValid()); Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(model()); Q_ASSERT(current.model() == prettyModel); for (bool forward : {true,false}) { QModelIndex walker = forward ? indexBelow(current) : indexAbove(current); while (walker.isValid()) { // Queued for pruning..? if (prettyModel->data(walker, Imap::Mailbox::RoleMessageUid).isValid()) { // Do not activate, just keep the cursor in place. selectionModel()->setCurrentIndex(walker, QItemSelectionModel::NoUpdate); // It has won. For now. return; } walker = forward ? indexBelow(walker) : indexAbove(walker); } } } void MsgListView::slotHandleSortCriteriaChanged(int column, Qt::SortOrder order) { // The if-clause is needed to prevent infinite recursion if (header()->sortIndicatorSection() != column || header()->sortIndicatorOrder() != order) { header()->setSortIndicator(column, order); } } /** @short Walk the hierarchy of proxy models up until we stop at the PrettyMsgListModel or the first non-proxy model */ Imap::Mailbox::PrettyMsgListModel *MsgListView::findPrettyMsgListModel(QAbstractItemModel *model) { while (QAbstractProxyModel *proxy = qobject_cast(model)) { Imap::Mailbox::PrettyMsgListModel *prettyModel = qobject_cast(proxy); if (prettyModel) return prettyModel; else model = proxy->sourceModel(); } return 0; } void MsgListView::setAutoActivateAfterKeyNavigation(bool enabled) { m_autoActivateAfterKeyNavigation = enabled; } } diff --git a/src/Gui/MsgListView.h b/src/Gui/MsgListView.h index 567b7653..172af19c 100644 --- a/src/Gui/MsgListView.h +++ b/src/Gui/MsgListView.h @@ -1,92 +1,93 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MSGLISTVIEW_H #define MSGLISTVIEW_H #include #include +#include "Imap/Model/FavoriteTagsModel.h" class QSignalMapper; namespace Imap { namespace Mailbox { class PrettyMsgListModel; } } namespace Gui { /** @short A slightly tweaked QTreeView optimized for showing a list of messages in one mailbox The optimizations (or rather modifications) include: - automatically expanding a whole subtree when root item is expanded - setting up reasonable size hints for all columns */ class MsgListView : public QTreeView { Q_OBJECT public: - explicit MsgListView(QWidget *parent=0); + explicit MsgListView(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel); virtual ~MsgListView() {} void setModel(QAbstractItemModel *model); void setAutoActivateAfterKeyNavigation(bool enabled); void updateActionsAfterRestoredState(); virtual int sizeHintForColumn(int column) const; QHeaderView::ResizeMode resizeModeForColumn(const int column) const; protected: void keyPressEvent(QKeyEvent *ke); void keyReleaseEvent(QKeyEvent *ke); virtual void startDrag(Qt::DropActions supportedActions); bool event(QEvent *event); private slots: void slotFixSize(); /** @short Expand all items below current root index */ void slotExpandWholeSubtree(const QModelIndex &rootIndex); /** @short Update header actions for showing/hiding columns */ void slotUpdateHeaderActions(); /** @short Show/hide a corresponding column */ void slotHeaderSectionVisibilityToggled(int section); /** @short Get ThreadingMsgListModel index and call the next handler */ void slotMsgListModelRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); /** @short Keep the cursor in place for better keyboard usability */ void slotThreadingMsgListModelRowAboutToBeRemoved(const QModelIndex &index); /** @short Pick up the change of the sort critera */ void slotHandleSortCriteriaChanged(int column, Qt::SortOrder order); /** @short conditionally emits activated(currentIndex()) for keyboard events */ void slotCurrentActivated(); void slotHandleNewColumns(int oldCount, int newCount); private: /** @short Try to move the cursor to next message */ void setCurrentIndexToNextValid(const QModelIndex ¤t); static Imap::Mailbox::PrettyMsgListModel *findPrettyMsgListModel(QAbstractItemModel *model); QSignalMapper *headerFieldsMapper; QTimer *m_naviActivationTimer; bool m_autoActivateAfterKeyNavigation; bool m_autoResizeSections; friend class MainWindow; // needs access to slotHandleNewColumns }; } #endif // MSGLISTVIEW_H diff --git a/src/Gui/Window.cpp b/src/Gui/Window.cpp index fad6aaf8..5e74dd7f 100644 --- a/src/Gui/Window.cpp +++ b/src/Gui/Window.cpp @@ -1,2900 +1,2900 @@ /* Copyright (C) 2006 - 2015 Jan Kundrát Copyright (C) 2013 - 2015 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "configure.cmake.h" #include "Common/Application.h" #include "Common/Paths.h" #include "Common/PortNumbers.h" #include "Common/SettingsNames.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #ifdef TROJITA_HAVE_CRYPTO_MESSAGES # ifdef TROJITA_HAVE_GPGMEPP # include "Cryptography/GpgMe++.h" # endif #endif #include "Imap/Model/ImapAccess.h" #include "Imap/Model/MailboxTree.h" #include "Imap/Model/Model.h" #include "Imap/Model/ModelWatcher.h" #include "Imap/Model/MsgListModel.h" #include "Imap/Model/NetworkWatcher.h" #include "Imap/Model/PrettyMailboxModel.h" #include "Imap/Model/PrettyMsgListModel.h" #include "Imap/Model/SpecialFlagNames.h" #include "Imap/Model/ThreadingMsgListModel.h" #include "Imap/Model/FavoriteTagsModel.h" #include "Imap/Model/Utils.h" #include "Imap/Tasks/ImapTask.h" #include "Imap/Network/FileDownloadManager.h" #include "MSA/ImapSubmit.h" #include "MSA/Sendmail.h" #include "MSA/SMTP.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PasswordPlugin.h" #include "Plugins/PluginManager.h" #include "CompleteMessageWidget.h" #include "ComposeWidget.h" #include "MailBoxTreeView.h" #include "MessageListWidget.h" #include "MessageView.h" #include "MessageSourceWidget.h" #include "Gui/MessageHeadersWidget.h" #include "MsgListView.h" #include "OnePanelAtTimeWidget.h" #include "PasswordDialog.h" #include "ProtocolLoggerWidget.h" #include "SettingsDialog.h" #include "SimplePartWidget.h" #include "Streams/SocketFactory.h" #include "TaskProgressIndicator.h" #include "Util.h" #include "Window.h" #include "ShortcutHandler/ShortcutHandler.h" #include "ui_CreateMailboxDialog.h" #include "ui_AboutDialog.h" #include "Imap/Model/ModelTest/modeltest.h" #include "UiUtils/IconLoader.h" #include "UiUtils/QaimDfsIterator.h" /** @short All user-facing widgets and related classes */ namespace Gui { static const char * const netErrorUnseen = "net_error_unseen"; MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0), m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0), m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0) { setAttribute(Qt::WA_AlwaysShowToolTips); // m_pluginManager must be created before calling createWidgets m_pluginManager = new Plugins::PluginManager(this, m_settings, Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin); connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged); connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) { Gui::Util::messageBoxWarning(this, tr("Plugin Error"), //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption. trUtf8("A plugin failed to load, therefore some functionality might be lost. " "You might want to update your system or report a bug to your vendor." "\n\n%1").arg(errorMessage)); }); #ifdef TROJITA_HAVE_CRYPTO_MESSAGES Plugins::PluginManager::MimePartReplacers replacers; #ifdef TROJITA_HAVE_GPGMEPP replacers.emplace_back(std::make_shared()); #endif m_pluginManager->setMimePartReplacers(replacers); #endif // ImapAccess contains a wrapper for retrieving passwords through some plugin. // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class, // which means that ImapAccess has to be constructed before we go and open the settings dialog. // FIXME: use another account-id at some point in future // we are now using the profile to avoid overwriting passwords of // other profiles in secure storage QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); m_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName); connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError); connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection); ShortcutHandler *shortcutHandler = new ShortcutHandler(this); shortcutHandler->setSettingsObject(m_settings); defineActions(); shortcutHandler->readSettings(); // must happen after defineActions() // must be created before calling createWidgets m_favoriteTags = new Imap::Mailbox::FavoriteTagsModel(this); m_favoriteTags->loadFromSettings(*m_settings); createWidgets(); Imap::migrateSettings(m_settings); m_senderIdentities = new Composer::SenderIdentitiesModel(this); m_senderIdentities->loadFromSettings(*m_settings); if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) { QTimer::singleShot(0, this, SLOT(slotShowSettings())); } setupModels(); createActions(); createMenus(); slotToggleSysTray(); slotPluginsChanged(); slotFavoriteTagsChanged(); connect(m_favoriteTags, &QAbstractItemModel::modelReset, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::layoutChanged, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsMoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsInserted, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsRemoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::dataChanged, this, &MainWindow::slotFavoriteTagsChanged); // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl"); QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact"); slotUpdateWindowTitle(); recoverDrafts(); if (m_actionLayoutWide->isEnabled() && m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) { m_actionLayoutWide->trigger(); } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) { m_actionLayoutOneAtTime->trigger(); } else { m_actionLayoutCompact->trigger(); } connect(qApp, &QGuiApplication::applicationStateChanged, this, [&](Qt::ApplicationState state) { if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } }); // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time. // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts // to that and only after the panel gets resized, the available size of "the rest" is correct again). // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially // because every change bumps the timer anyway, as Thomas pointed out). QTimer *delayedResize = new QTimer(this); delayedResize->setSingleShot(true); delayedResize->setInterval(500); connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged); connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast(&QTimer::start)); m_skipSavingOfUI = false; } void MainWindow::defineActions() { ShortcutHandler *shortcutHandler = ShortcutHandler::instance(); shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit); shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New); shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft...")); shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M")); shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M")); shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N")); shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString()); shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J")); shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U")); shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A")); shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L")); shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R")); shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F")); shortcutHandler->defineAction(QStringLiteral("action_bounce"), QStringLiteral("mail-bounce"), tr("Edit as New E-Mail Message...")); shortcutHandler->defineAction(QStringLiteral("action_archive"), QStringLiteral("mail-move-to-archive"), tr("&Archive"), QStringLiteral("A")); shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book...")); shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline")); shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection")); shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access")); shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window")); shortcutHandler->defineAction(QStringLiteral("action_open_messagewindow"), QString(), tr("Open message in New Window..."), QStringLiteral("Ctrl+Return")); shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString()); shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn); shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut); shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size")); shortcutHandler->defineAction(QStringLiteral("action_focus_mailbox_tree"), QString(), tr("Move Focus to Mailbox List")); shortcutHandler->defineAction(QStringLiteral("action_focus_msg_list"), QString(), tr("Move Focus to Message List")); shortcutHandler->defineAction(QStringLiteral("action_tag_1"), QStringLiteral("mail-tag-1"), tr("Tag with &1st tag"), QStringLiteral("1")); shortcutHandler->defineAction(QStringLiteral("action_tag_2"), QStringLiteral("mail-tag-2"), tr("Tag with &2nd tag"), QStringLiteral("2")); shortcutHandler->defineAction(QStringLiteral("action_tag_3"), QStringLiteral("mail-tag-3"), tr("Tag with &3rd tag"), QStringLiteral("3")); shortcutHandler->defineAction(QStringLiteral("action_tag_4"), QStringLiteral("mail-tag-4"), tr("Tag with &4th tag"), QStringLiteral("4")); shortcutHandler->defineAction(QStringLiteral("action_tag_5"), QStringLiteral("mail-tag-5"), tr("Tag with &5th tag"), QStringLiteral("5")); shortcutHandler->defineAction(QStringLiteral("action_tag_6"), QStringLiteral("mail-tag-6"), tr("Tag with &6th tag"), QStringLiteral("6")); shortcutHandler->defineAction(QStringLiteral("action_tag_7"), QStringLiteral("mail-tag-7"), tr("Tag with &7th tag"), QStringLiteral("7")); shortcutHandler->defineAction(QStringLiteral("action_tag_8"), QStringLiteral("mail-tag-8"), tr("Tag with &8th tag"), QStringLiteral("8")); shortcutHandler->defineAction(QStringLiteral("action_tag_9"), QStringLiteral("mail-tag-9"), tr("Tag with &9th tag"), QStringLiteral("9")); } void MainWindow::createActions() { // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default: // // Thunderbird: // private: Ctrl+R // all: Ctrl+Shift+R // list: Ctrl+Shift+L // forward: Ctrl+L // (no shortcuts for type of forwarding) // bounce: ctrl+B // new message: Ctrl+N // // KMail: // "reply": R // private: Shift+A // all: A // list: L // forward as attachment: F // forward inline: Shift+F // bounce: E // new: Ctrl+N m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this); m_mainToolbar = addToolBar(tr("Navigation")); m_mainToolbar->setObjectName(QStringLiteral("mainToolbar")); reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this); connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList); resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this); connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox); reloadAllMailboxes = new QAction(tr("&Reload Everything"), this); // connect later exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this); exitAction->setStatusTip(tr("Exit the application")); netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline")); netOffline->setCheckable(true); // connect later netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive")); netExpensive->setCheckable(true); // connect later netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online")); netOnline->setCheckable(true); // connect later QActionGroup *netPolicyGroup = new QActionGroup(this); netPolicyGroup->setExclusive(true); netPolicyGroup->addAction(netOffline); netPolicyGroup->addAction(netExpensive); netPolicyGroup->addAction(netOnline); //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this); showFullView->setCheckable(true); connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible); connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked); //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox showTaskView = new QAction(tr("Show ImapTask t&ree"), this); showTaskView->setCheckable(true); connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible); connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked); //: a debugging tool showing the mime tree of the current message showMimeView = new QAction(tr("Show &MIME tree"), this); showMimeView->setCheckable(true); connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible); connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked); showImapLogger = new QAction(tr("Show IMAP protocol &log"), this); showImapLogger->setCheckable(true); connect(showImapLogger, &QAction::toggled, imapLoggerDock, &QWidget::setVisible); connect(imapLoggerDock, &QDockWidget::visibilityChanged, showImapLogger, &QAction::setChecked); //: file to save the debug log into logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this); logPersistent->setCheckable(true); connect(logPersistent, &QAction::triggered, imapLogger, &ProtocolLoggerWidget::slotSetPersistentLogging); connect(imapLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked); showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this); connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo); showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this); showMenuBar->setCheckable(true); showMenuBar->setChecked(true); connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible); connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast(&QTimer::start)); showToolBar = new QAction(tr("Show &Toolbar"), this); showToolBar->setCheckable(true); connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible); connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked); connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast(&QTimer::start)); configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")), tr("&Settings..."), this); connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings); QAction *triggerSearch = new QAction(this); addAction(triggerSearch); triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, ="))); connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch); triggerSearch = new QAction(this); addAction(triggerSearch); triggerSearch->setShortcut(QKeySequence(QStringLiteral("/"))); connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusSearch); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_mailbox_tree"), mboxTree, SLOT(setFocus()), this)); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_msg_list"), msgListWidget->tree, SLOT(setFocus()), this)); m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this); m_oneAtTimeGoBack->setEnabled(false); composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this); m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this); expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this); m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this); m_bounce = ShortcutHandler::instance()->createAction(QStringLiteral("action_bounce"), this, SLOT(slotBounce()), this); markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this); markAsRead->setCheckable(true); msgListWidget->tree->addAction(markAsRead); connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead); m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this); msgListWidget->tree->addAction(m_nextMessage); m_messageWidget->messageView->addAction(m_nextMessage); m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this); msgListWidget->tree->addAction(m_previousMessage); m_messageWidget->messageView->addAction(m_previousMessage); markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this); markAsDeleted->setCheckable(true); msgListWidget->tree->addAction(markAsDeleted); connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted); markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this); markAsFlagged->setCheckable(true); connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged); markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this); markAsJunk->setCheckable(true); connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk); markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this); markAsNotJunk->setCheckable(true); connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk); saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this); msgListWidget->tree->addAction(saveWholeMessage); viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this); msgListWidget->tree->addAction(viewMsgSource); viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this); msgListWidget->tree->addAction(viewMsgHeaders); msgListWidget->tree->addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_open_messagewindow"), this, SLOT(openCompleteMessageWidget()), this)); moveToArchive = ShortcutHandler::instance()->createAction(QStringLiteral("action_archive"), this); connect(moveToArchive, &QAction::triggered, this, &MainWindow::handleMoveToArchive); auto addTagAction = [=](int row) { QAction *tag = ShortcutHandler::instance()->createAction(QStringLiteral("action_tag_").append(QString::number(row)), this); tag->setCheckable(true); msgListWidget->tree->addAction(tag); connect(tag, &QAction::triggered, this, [=](const bool checked) { handleTag(checked, row - 1); }); return tag; }; tag1 = addTagAction(1); tag2 = addTagAction(2); tag3 = addTagAction(3); tag4 = addTagAction(4); tag5 = addTagAction(5); tag6 = addTagAction(6); tag7 = addTagAction(7); tag8 = addTagAction(8); tag9 = addTagAction(9); //: "mailbox" as a "folder of messages", not as a "mail account" createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this); connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent); //: "mailbox" as a "folder of messages", not as a "mail account" createTopMailbox = new QAction(tr("Create &New Mailbox..."), this); connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox); m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this); connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead); //: "mailbox" as a "folder of messages", not as a "mail account" deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this); connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox); #ifdef XTUPLE_CONNECT xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this); xtIncludeMailboxInSync->setCheckable(true); connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox())); #endif m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this); m_replyPrivate->setEnabled(false); m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this); m_replyAllButMe->setEnabled(false); m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this); m_replyAll->setEnabled(false); m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this); m_replyList->setEnabled(false); m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this); m_replyGuess->setEnabled(true); actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this); actionThreadMsgList->setCheckable(true); // This action is enabled/disabled by model's capabilities actionThreadMsgList->setEnabled(false); if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) { actionThreadMsgList->setChecked(true); // The actual threading will be performed only when model updates its capabilities } connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList); QActionGroup *sortOrderGroup = new QActionGroup(this); m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup); m_actionSortAscending->setCheckable(true); m_actionSortAscending->setChecked(true); m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup); m_actionSortDescending->setCheckable(true); // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-) connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent); connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); QActionGroup *sortColumnGroup = new QActionGroup(this); m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup); m_actionSortNone->setCheckable(true); m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup); m_actionSortThreading->setCheckable(true); m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup); m_actionSortByArrival->setCheckable(true); m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup); m_actionSortByCc->setCheckable(true); m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup); m_actionSortByDate->setCheckable(true); m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup); m_actionSortByFrom->setCheckable(true); m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup); m_actionSortBySize->setCheckable(true); m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup); m_actionSortBySubject->setCheckable(true); m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup); m_actionSortByTo->setCheckable(true); connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); slotSortingConfirmed(-1, Qt::AscendingOrder); actionHideRead = new QAction(tr("&Hide Read Messages"), this); actionHideRead->setCheckable(true); if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) { actionHideRead->setChecked(true); prettyMsgListModel->setHideRead(true); } connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead); QActionGroup *layoutGroup = new QActionGroup(this); m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup); m_actionLayoutCompact->setCheckable(true); m_actionLayoutCompact->setChecked(true); connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact); m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup); m_actionLayoutWide->setCheckable(true); connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide); m_actionLayoutOneAtTime = new QAction(tr("&One At Time"), layoutGroup); m_actionLayoutOneAtTime->setCheckable(true); connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime); m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this); m_actionShowOnlySubscribed->setCheckable(true); m_actionShowOnlySubscribed->setEnabled(false); connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed); m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this); m_actionSubscribeMailbox->setCheckable(true); m_actionSubscribeMailbox->setEnabled(false); connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox); aboutTrojita = new QAction(trUtf8("&About Trojitá..."), this); connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita); donateToTrojita = new QAction(tr("&Donate to the project"), this); connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita); connectModelActions(); m_composeMenu = new QMenu(tr("Compose Mail"), this); m_composeMenu->addAction(composeMail); m_composeMenu->addAction(m_editDraft); m_composeButton = new QToolButton(this); m_composeButton->setPopupMode(QToolButton::MenuButtonPopup); m_composeButton->setMenu(m_composeMenu); m_composeButton->setDefaultAction(composeMail); m_replyButton = new QToolButton(this); m_replyButton->setPopupMode(QToolButton::MenuButtonPopup); m_replyMenu = new QMenu(m_replyButton); m_replyMenu->addAction(m_replyPrivate); m_replyMenu->addAction(m_replyAllButMe); m_replyMenu->addAction(m_replyAll); m_replyMenu->addAction(m_replyList); m_replyButton->setMenu(m_replyMenu); m_replyButton->setDefaultAction(m_replyPrivate); m_mainToolbar->addWidget(m_composeButton); m_mainToolbar->addWidget(m_replyButton); m_mainToolbar->addAction(m_forwardAsAttachment); m_mainToolbar->addAction(expunge); m_mainToolbar->addSeparator(); m_mainToolbar->addAction(markAsRead); m_mainToolbar->addAction(markAsDeleted); m_mainToolbar->addAction(markAsFlagged); m_mainToolbar->addAction(markAsJunk); m_mainToolbar->addAction(markAsNotJunk); m_mainToolbar->addAction(moveToArchive); // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom. QWidget *toolbarSpacer = new QWidget(m_mainToolbar); toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mainToolbar->addWidget(toolbarSpacer); m_mainToolbar->addSeparator(); m_mainToolbar->addWidget(busyParsersIndicator); networkIndicator = new QToolButton(this); // This is deliberate; we want to show this button in the same style as the other ones in the toolbar networkIndicator->setPopupMode(QToolButton::MenuButtonPopup); m_mainToolbar->addWidget(networkIndicator); m_menuFromToolBar = new QToolButton(this); m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new"))); m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON' m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup); connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu); m_mainToolbar->addWidget(m_menuFromToolBar); connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) { // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton m_mainToolbar->actions().last()->setVisible(!menuBarVisible); }); m_mainToolbar->actions().last()->setVisible(false); // initial state to complement the default of the QMenuBar's visibility busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize()); { // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified. // That sucks, because some of our widgets use multiple actions with an expanding arrow at right. // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows // are, well, at right. // I have no idea how this works on RTL layouts. QLayout *lay = m_mainToolbar->layout(); for (int i = 0; i < lay->count(); ++i) { QLayoutItem *it = lay->itemAt(i); if (it->widget() == toolbarSpacer) { // Don't align this one, otherwise it won't push stuff when in horizontal direction continue; } if (it->widget() == busyParsersIndicator) { // It looks much better when centered it->setAlignment(Qt::AlignJustify); continue; } it->setAlignment(Qt::AlignLeft); } } updateMessageFlags(); } void MainWindow::connectModelActions() { connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList); connect(netOffline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline); connect(netExpensive, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive); connect(netOnline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline); netExpensive->setEnabled(imapAccess()->isConfigured()); netOnline->setEnabled(imapAccess()->isConfigured()); } void MainWindow::createMenus() { #define ADD_ACTION(MENU, ACTION) \ MENU->addAction(ACTION); \ addAction(ACTION); QMenu *imapMenu = menuBar()->addMenu(tr("&IMAP")); imapMenu->addMenu(m_composeMenu); ADD_ACTION(imapMenu, m_actionContactEditor); ADD_ACTION(imapMenu, m_replyGuess); ADD_ACTION(imapMenu, m_replyPrivate); ADD_ACTION(imapMenu, m_replyAll); ADD_ACTION(imapMenu, m_replyAllButMe); ADD_ACTION(imapMenu, m_replyList); imapMenu->addSeparator(); ADD_ACTION(imapMenu, m_forwardAsAttachment); ADD_ACTION(imapMenu, m_bounce); imapMenu->addSeparator(); ADD_ACTION(imapMenu, expunge); imapMenu->addSeparator()->setText(tr("Network Access")); QMenu *netPolicyMenu = imapMenu->addMenu(tr("&Network Access")); ADD_ACTION(netPolicyMenu, netOffline); ADD_ACTION(netPolicyMenu, netExpensive); ADD_ACTION(netPolicyMenu, netOnline); QMenu *debugMenu = imapMenu->addMenu(tr("&Debugging")); ADD_ACTION(debugMenu, showFullView); ADD_ACTION(debugMenu, showTaskView); ADD_ACTION(debugMenu, showMimeView); ADD_ACTION(debugMenu, showImapLogger); ADD_ACTION(debugMenu, logPersistent); ADD_ACTION(debugMenu, showImapCapabilities); imapMenu->addSeparator(); ADD_ACTION(imapMenu, configSettings); ADD_ACTION(imapMenu, ShortcutHandler::instance()->shortcutConfigAction()); imapMenu->addSeparator(); ADD_ACTION(imapMenu, exitAction); QMenu *viewMenu = menuBar()->addMenu(tr("&View")); ADD_ACTION(viewMenu, showMenuBar); ADD_ACTION(viewMenu, showToolBar); QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout")); ADD_ACTION(layoutMenu, m_actionLayoutCompact); ADD_ACTION(layoutMenu, m_actionLayoutWide); ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime); viewMenu->addSeparator(); ADD_ACTION(viewMenu, m_previousMessage); ADD_ACTION(viewMenu, m_nextMessage); viewMenu->addSeparator(); QMenu *sortMenu = viewMenu->addMenu(tr("S&orting")); ADD_ACTION(sortMenu, m_actionSortNone); ADD_ACTION(sortMenu, m_actionSortThreading); ADD_ACTION(sortMenu, m_actionSortByArrival); ADD_ACTION(sortMenu, m_actionSortByCc); ADD_ACTION(sortMenu, m_actionSortByDate); ADD_ACTION(sortMenu, m_actionSortByFrom); ADD_ACTION(sortMenu, m_actionSortBySize); ADD_ACTION(sortMenu, m_actionSortBySubject); ADD_ACTION(sortMenu, m_actionSortByTo); sortMenu->addSeparator(); ADD_ACTION(sortMenu, m_actionSortAscending); ADD_ACTION(sortMenu, m_actionSortDescending); ADD_ACTION(viewMenu, actionThreadMsgList); ADD_ACTION(viewMenu, actionHideRead); ADD_ACTION(viewMenu, m_actionShowOnlySubscribed); QMenu *mailboxMenu = menuBar()->addMenu(tr("&Mailbox")); ADD_ACTION(mailboxMenu, resyncMbox); mailboxMenu->addSeparator(); ADD_ACTION(mailboxMenu, reloadAllMailboxes); QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); ADD_ACTION(helpMenu, donateToTrojita); helpMenu->addSeparator(); ADD_ACTION(helpMenu, aboutTrojita); QMenu *mainMenuBehindToolBar = new QMenu(this); m_menuFromToolBar->setMenu(mainMenuBehindToolBar); m_menuFromToolBar->menu()->addMenu(imapMenu); m_menuFromToolBar->menu()->addMenu(viewMenu); m_menuFromToolBar->menu()->addMenu(mailboxMenu); m_menuFromToolBar->menu()->addMenu(helpMenu); m_menuFromToolBar->menu()->addSeparator(); m_menuFromToolBar->menu()->addAction(showMenuBar); networkIndicator->setMenu(netPolicyMenu); m_netToolbarDefaultAction = new QAction(this); networkIndicator->setDefaultAction(m_netToolbarDefaultAction); connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu); connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); #undef ADD_ACTION } void MainWindow::createWidgets() { // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure // we do not hit the disk continually when e.g. resizing some random widget. m_delayedStateSaving = new QTimer(this); m_delayedStateSaving->setInterval(1000); m_delayedStateSaving->setSingleShot(true); connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState); mboxTree = new MailBoxTreeView(); mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList()); connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree); connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) { m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames); }); - msgListWidget = new MessageListWidget(); + msgListWidget = new MessageListWidget(nullptr, m_favoriteTags); msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu); msgListWidget->tree->setAlternatingRowColors(true); msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool()); connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting); connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree); connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::openCompleteMessageWidget); connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested); connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast(&QTimer::start)); msgListWidget->tree->installEventFilter(this); m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager, m_favoriteTags); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) { if (url.isEmpty()) { QToolTip::hideText(); } else { // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783 QTimer::singleShot(250, [url]() { QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url))); }); } }); #endif connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError); // Do not try to get onto the homepage when we are on EXPENSIVE connection if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() && m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) { m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version))); } allDock = new QDockWidget(tr("Everything"), this); allDock->setObjectName(QStringLiteral("allDock")); allTree = new QTreeView(allDock); allDock->hide(); allTree->setUniformRowHeights(true); allTree->setHeaderHidden(true); allDock->setWidget(allTree); addDockWidget(Qt::LeftDockWidgetArea, allDock); taskDock = new QDockWidget(tr("IMAP Tasks"), this); taskDock->setObjectName(QStringLiteral("taskDock")); taskTree = new QTreeView(taskDock); taskDock->hide(); taskTree->setHeaderHidden(true); taskDock->setWidget(taskTree); addDockWidget(Qt::LeftDockWidgetArea, taskDock); mailMimeDock = new QDockWidget(tr("MIME Tree"), this); mailMimeDock->setObjectName(QStringLiteral("mailMimeDock")); mailMimeTree = new QTreeView(mailMimeDock); mailMimeDock->hide(); mailMimeTree->setUniformRowHeights(true); mailMimeTree->setHeaderHidden(true); mailMimeDock->setWidget(mailMimeTree); addDockWidget(Qt::RightDockWidgetArea, mailMimeDock); connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged); imapLoggerDock = new QDockWidget(tr("IMAP Protocol"), this); imapLoggerDock->setObjectName(QStringLiteral("imapLoggerDock")); imapLogger = new ProtocolLoggerWidget(imapLoggerDock); imapLoggerDock->hide(); imapLoggerDock->setWidget(imapLogger); addDockWidget(Qt::BottomDockWidgetArea, imapLoggerDock); busyParsersIndicator = new TaskProgressIndicator(this); } void MainWindow::setupModels() { m_imapAccess->reloadConfiguration(); m_imapAccess->doConnect(); m_messageWidget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); auto realThreadingModel = qobject_cast(m_imapAccess->threadingMsgListModel()); Q_ASSERT(realThreadingModel); auto realMsgListModel = qobject_cast(m_imapAccess->msgListModel()); Q_ASSERT(realMsgListModel); prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast(m_imapAccess->mailboxModel())); prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel")); connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed, msgListWidget, &MessageListWidget::slotSortingFailed); - prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this, m_favoriteTags); + prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this); prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel()); prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel")); connect(mboxTree, &MailBoxTreeView::clicked, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(mboxTree, &MailBoxTreeView::activated, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags); connect(qobject_cast(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable, this, &MainWindow::slotScrollToUnseenMessage); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags); connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged); connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived); connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError); connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError); connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection); connect(imapModel(), &Imap::Mailbox::Model::authAttemptFailed, this, [this]() { m_ignoreStoredPassword = true; }); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, [this](uint, const Imap::ConnectionState state) { if (state == Imap::CONN_STATE_AUTHENTICATED) { m_ignoreStoredPassword = false; } }); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus); connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed); connect(imapModel(), &Imap::Mailbox::Model::logged, imapLogger, &ProtocolLoggerWidget::slotImapLogged); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, imapLogger, &ProtocolLoggerWidget::onConnectionClosed); auto nw = qobject_cast(m_imapAccess->networkWatcher()); Q_ASSERT(nw); connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled, this, [this](const int timeout) { showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000)); }); connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState); connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage); connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle); connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed); //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this ); //w->setModel( imapModel() ); //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time mboxTree->setModel(prettyMboxModel); msgListWidget->tree->setModel(prettyMsgListModel); connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags); allTree->setModel(imapModel()); taskTree->setModel(imapModel()->taskModel()); connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll); busyParsersIndicator->setImapModel(imapModel()); auto accountIconName = m_settings->value(Common::SettingsNames::imapAccountIcon).toString(); if (accountIconName.isEmpty()) { qApp->setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita"))); } else if (accountIconName.contains(QDir::separator())) { // Absolute paths are OK for users, but unsupported by our icon loader qApp->setWindowIcon(QIcon(accountIconName)); } else { qApp->setWindowIcon(UiUtils::loadIcon(accountIconName)); } } void MainWindow::createSysTray() { if (m_trayIcon) return; qApp->setQuitOnLastWindowClosed(false); m_trayIcon = new QSystemTrayIcon(this); handleTrayIconChange(); QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon); connect(quitAction, &QAction::triggered, qApp, &QApplication::quit); QMenu *trayIconMenu = new QMenu(this); trayIconMenu->addAction(quitAction); m_trayIcon->setContextMenu(trayIconMenu); // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope. connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater); connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange); m_trayIcon->setVisible(true); m_trayIcon->show(); } void MainWindow::removeSysTray() { delete m_trayIcon; m_trayIcon = 0; qApp->setQuitOnLastWindowClosed(true); } void MainWindow::slotToggleSysTray() { bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool(); if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) { createSysTray(); } else if (!showSystray && m_trayIcon) { removeSysTray(); } } void MainWindow::handleTrayIconChange() { if (!m_trayIcon) return; const bool isOffline = qobject_cast(m_imapAccess->networkWatcher())->effectiveNetworkPolicy() == Imap::Mailbox::NETWORK_OFFLINE; auto pixmap = qApp->windowIcon() .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal); QString tooltip; auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (profileName.isEmpty()) { tooltip = QStringLiteral("Trojitá"); } else { tooltip = QStringLiteral("Trojitá [%1]").arg(profileName); } uint unreadCount = 0; bool numbersValid = false; auto watchingMode = settings()->value(Common::SettingsNames::watchedFoldersKey).toString(); if (watchingMode == Common::SettingsNames::watchAll || watchingMode == Common::SettingsNames::watchSubscribed) { bool subscribedOnly = watchingMode == Common::SettingsNames::watchSubscribed; unreadCount = std::accumulate(UiUtils::QaimDfsIterator(m_imapAccess->mailboxModel()->index(0, 0)), UiUtils::QaimDfsIterator(), 0, [subscribedOnly](const uint acc, const QModelIndex &idx) { if (subscribedOnly && !idx.data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()) return acc; auto x = idx.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); if (x > 0) { return acc + x; } else { return acc; } }); // only show stuff if there are some mailboxes, and if there are such messages numbersValid = m_imapAccess->mailboxModel()->hasChildren() && unreadCount > 0; } else { // just for the INBOX QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex()); if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX") && mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) { unreadCount = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); numbersValid = true; } } if (numbersValid) { QFont f; f.setPixelSize(pixmap.height() * 0.59); f.setWeight(QFont::Bold); QString text = QString::number(unreadCount); QFontMetrics fm(f); if (unreadCount > 666) { // You just have too many messages. text = QStringLiteral("🐮"); fm = QFontMetrics(f); } else if (fm.width(text) > pixmap.width()) { f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text)); fm = QFontMetrics(f); } QRect boundingRect = fm.tightBoundingRect(text); boundingRect.setWidth(boundingRect.width() + 2); boundingRect.setHeight(boundingRect.height() + 2); boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2)); boundingRect = boundingRect.intersected(pixmap.rect()); QPainterPath path; path.addText(boundingRect.bottomLeft(), f, text); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QColor(255,255,255, 180)); painter.setBrush(isOffline ? Qt::red : Qt::black); painter.drawPath(path); //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]" tooltip += trUtf8(" - %n unread message(s)", 0, unreadCount); } else if (isOffline) { //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]" tooltip += tr(" - offline"); } m_trayIcon->setToolTip(tooltip); m_trayIcon->setIcon(QIcon(pixmap)); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_trayIcon && m_trayIcon->isVisible()) { Util::askForSomethingUnlessTold(trUtf8("Trojitá"), tr("The application will continue in systray. This can be disabled within the settings."), Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings); hide(); event->ignore(); } } bool MainWindow::eventFilter(QObject *o, QEvent *e) { if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) { if (e->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(e); if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) { QCoreApplication::sendEvent(m_messageWidget, keyEvent); return true; } return false; } return false; } if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) { // installed if sorting is not really possible. QWidget *header = static_cast(o); QMouseEvent *mouse = static_cast(e); if (e->type() == QEvent::MouseButtonPress) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) { m_headerDragStart = mouse->pos(); } return false; } if (e->type() == QEvent::MouseButtonRelease) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor && (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) { m_actionSortDescending->toggle(); Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; msgListWidget->tree->header()->setSortIndicator(-1, order); return true; // prevent regular click } } } return false; } void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { setVisible(!isVisible()); if (isVisible()) showMainWindow(); } } void MainWindow::showMainWindow() { setVisible(true); activateWindow(); raise(); } void MainWindow::msgListClicked(const QModelIndex &index) { Q_ASSERT(index.isValid()); if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier) return; if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that // user will be notified about new arrivals, etc. QModelIndex translated = Imap::deproxifiedIndex(index); imapModel()->switchToMailbox(translated.parent().parent()); if (index.column() == Imap::Mailbox::MsgListModel::SEEN) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp); if (translated == m_messageWidget->messageView->currentMessage()) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp); } else { if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) { // isVisible() won't work, the splitter manipulates width, not the visibility state m_messageWidget->messageView->setMessage(index); } msgListWidget->tree->setCurrentIndex(index); } } void MainWindow::openCompleteMessageWidget() { const QModelIndex index = msgListWidget->tree->currentIndex(); if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager, m_favoriteTags); widget->messageView->setMessage(index); widget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); widget->setFocusPolicy(Qt::StrongFocus); widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString()); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget); widget->addAction(closeAction); widget->show(); } void MainWindow::showContextMenuMboxTree(const QPoint &position) { QList actionList; if (mboxTree->indexAt(position).isValid()) { actionList.append(createChildMailbox); actionList.append(deleteCurrentMailbox); actionList.append(m_actionMarkMailboxAsRead); actionList.append(resyncMbox); actionList.append(reloadMboxList); actionList.append(m_actionSubscribeMailbox); m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()); #ifdef XTUPLE_CONNECT actionList.append(xtIncludeMailboxInSync); xtIncludeMailboxInSync->setChecked( m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains( mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString())); #endif } else { actionList.append(createTopMailbox); } actionList.append(reloadAllMailboxes); actionList.append(m_actionShowOnlySubscribed); QMenu::exec(actionList, mboxTree->mapToGlobal(position), nullptr, this); } void MainWindow::showContextMenuMsgListTree(const QPoint &position) { QList actionList; QModelIndex index = msgListWidget->tree->indexAt(position); if (index.isValid()) { updateMessageFlagsOf(index); actionList.append(markAsRead); actionList.append(markAsDeleted); actionList.append(markAsFlagged); actionList.append(markAsJunk); actionList.append(markAsNotJunk); actionList.append(moveToArchive); actionList.append(m_actionMarkMailboxAsRead); actionList.append(saveWholeMessage); actionList.append(viewMsgSource); actionList.append(viewMsgHeaders); auto appendTagIfExists = [this,&actionList](const int row, QAction *tag) { if (m_favoriteTags->rowCount() > row - 1) actionList.append(tag); }; appendTagIfExists(1, tag1); appendTagIfExists(2, tag2); appendTagIfExists(3, tag3); appendTagIfExists(4, tag4); appendTagIfExists(5, tag5); appendTagIfExists(6, tag6); appendTagIfExists(7, tag7); appendTagIfExists(8, tag8); appendTagIfExists(9, tag9); } if (! actionList.isEmpty()) QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position), nullptr, this); } /** @short Ask for an updated list of mailboxes situated below the selected one */ void MainWindow::slotReloadMboxList() { Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast( Imap::Mailbox::Model::realTreeItem(item) ); Q_ASSERT(mbox); mbox->rescanForChildMailboxes(imapModel()); } } /** @short Request a check for new messages in selected mailbox */ void MainWindow::slotResyncMbox() { if (! imapModel()->isNetworkAvailable()) return; Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; imapModel()->resyncMailbox(item); } } void MainWindow::alertReceived(const QString &message) { //: "ALERT" is a special warning which we're required to show to the user Gui::Util::messageBoxWarning(this, tr("IMAP Alert"), message); } void MainWindow::imapError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Protocol Error"), message); // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report. // showImapLogger->setChecked(true); } void MainWindow::networkError(const QString &message) { const QString title = tr("Network Error"); if (!m_networkErrorMessageBox) { m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title, QString(), QMessageBox::Ok, this); } // User must be informed about a new (but not recurring) error if (message != m_networkErrorMessageBox->text()) { m_networkErrorMessageBox->setText(message); if (qApp->applicationState() == Qt::ApplicationActive) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } else { m_networkErrorMessageBox->setProperty(netErrorUnseen, true); if (m_trayIcon && m_trayIcon->isVisible()) m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333); } } } void MainWindow::cacheError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Cache Error"), tr("The caching subsystem managing a cache of the data already " "downloaded from the IMAP server is having troubles. " "All caching will be disabled.\n\n%1").arg(message)); } void MainWindow::networkPolicyOffline() { netExpensive->setChecked(false); netOnline->setChecked(false); netOffline->setChecked(true); updateActionsOnlineOffline(false); showStatusMessage(tr("Offline")); handleTrayIconChange(); } void MainWindow::networkPolicyExpensive() { netOffline->setChecked(false); netOnline->setChecked(false); netExpensive->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } void MainWindow::networkPolicyOnline() { netOffline->setChecked(false); netExpensive->setChecked(false); netOnline->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } /** @short Deletes a network error message box instance upon resetting of reconnect state */ void MainWindow::slotResetReconnectState() { if (m_networkErrorMessageBox) { delete m_networkErrorMessageBox; m_networkErrorMessageBox = 0; } } void MainWindow::slotShowSettings() { SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_favoriteTags, m_settings); if (dialog->exec() == QDialog::Accepted) { // FIXME: wipe cache in case we're moving between servers nukeModels(); setupModels(); connectModelActions(); // The systray is still connected to the old model -- got to make sure it's getting updated removeSysTray(); slotToggleSysTray(); } QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString(); if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL && method != Common::SettingsNames::methodProcess ) { Gui::Util::messageBoxCritical(this, tr("No Configuration"), trUtf8("No IMAP account is configured. Trojitá cannot do much without one.")); } applySizesAndState(); } void MainWindow::authenticationRequested() { Plugins::PasswordPlugin *password = pluginManager()->password(); if (password) { // FIXME: use another account-id at some point in future // Currently the accountName will be empty unless Trojita has been // called with a profile, and then the profile will be used as the // accountName. QString accountName = m_imapAccess->accountName(); if (accountName.isEmpty()) accountName = QStringLiteral("account-0"); Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap")); if (job) { connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) { authenticationContinue(password); }); connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) { if (error == Plugins::PasswordJob::Error::NoSuchPassword) { authenticationContinue(QString()); } else { authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message)); } }); job->setAutoDelete(true); job->start(); return; } } authenticationContinue(QString()); } void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage) { const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString(); QString pass = password; if (m_ignoreStoredPassword || pass.isEmpty()) { auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

Please provide IMAP password for user %1 on %2:

").arg( user.toHtmlEscaped(), m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped() ), errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n")) + imapModel()->imapAuthError()); connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword); connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword); } else { imapModel()->setImapPassword(pass); } } void MainWindow::checkSslPolicy() { m_imapAccess->setSslPolicy(QMessageBox(static_cast(m_imapAccess->sslInfoIcon()), m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(), QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes); } void MainWindow::nukeModels() { m_messageWidget->messageView->setEmpty(); mboxTree->setModel(0); msgListWidget->tree->setModel(0); allTree->setModel(0); taskTree->setModel(0); delete prettyMsgListModel; prettyMsgListModel = 0; delete prettyMboxModel; prettyMboxModel = 0; } void MainWindow::recoverDrafts() { QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft"))); Q_FOREACH(const QString &draft, drafts) { ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this); // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured if (!w) break; } } void MainWindow::slotComposeMail() { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this); } void MainWindow::slotEditDraft() { QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts")); QDir().mkpath(path); path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)")); if (!path.isNull()) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this); } } QModelIndexList MainWindow::translatedSelection() const { QModelIndexList translatedIndexes; QModelIndexList selected = msgListWidget->tree->selectionModel()->selectedIndexes(); const int originalItems = selected.length(); // only check collapsed/expanded status on original selection for (int i = 0; i < selected.length(); ++i) { const QModelIndex item = selected[i]; if (item.column() != 0 || !item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; translatedIndexes << Imap::deproxifiedIndex(item); // Now see if this is a collapsed thread and include all the collapsed items as needed // Also note that this is recursive - each child found is run through this same item loop for validity/child checks as well if (i >= originalItems || !msgListWidget->tree->isExpanded(item)) { for (int j = 0; j < item.model()->rowCount(item); ++j) { selected << item.child(j, 0); // Make sure this is run through the main loop as well - don't add it directly } } } return translatedIndexes; } void MainWindow::handleMarkAsRead(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsRead: no valid messages"; } else { imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } } void MainWindow::slotNextUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoNext(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::slotPreviousUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoPrevious(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::handleTag(const bool checked, const int index) { const QModelIndexList &translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleTag: no valid messages"; } else { const auto &tagName = m_favoriteTags->tagNameByIndex(index); if (!tagName.isEmpty()) imapModel()->setMessageFlags(translatedIndexes, tagName, checked ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsDeleted(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsDeleted: no valid messages"; } else { imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsFlagged(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsFlagged: no valid messages"; } else { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsNotJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsNotJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::slotMoveToArchiveFailed(const QString &error) { // XXX disable busy cursor QMessageBox::critical(this, tr("Failed to archive"), error); } void MainWindow::handleMoveToArchive() { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMoveToArchive: no valid messages"; } else { auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); auto copyMoveMessagesTask = imapModel()->copyMoveMessages( archiveFolderName.isEmpty() ? Common::SettingsNames::imapDefaultArchiveFolderName : archiveFolderName, translatedIndexes, Imap::Mailbox::CopyMoveOperation::MOVE); connect(copyMoveMessagesTask, &Imap::Mailbox::ImapTask::failed, this, &MainWindow::slotMoveToArchiveFailed); } } void MainWindow::slotExpunge() { imapModel()->expungeMailbox(qobject_cast(m_imapAccess->msgListModel())->currentMailbox()); } void MainWindow::slotMarkCurrentMailboxRead() { imapModel()->markMailboxAsRead(mboxTree->currentIndex()); } void MainWindow::slotCreateMailboxBelowCurrent() { createMailboxBelow(mboxTree->currentIndex()); } void MainWindow::slotCreateTopMailbox() { createMailboxBelow(QModelIndex()); } void MainWindow::createMailboxBelow(const QModelIndex &index) { Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ? dynamic_cast( Imap::Mailbox::Model::realTreeItem(index)) : 0; Ui::CreateMailboxDialog ui; QDialog *dialog = new QDialog(this); ui.setupUi(dialog); dialog->setWindowTitle(mboxPtr ? tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) : tr("Create a Top-level Mailbox")); if (dialog->exec() == QDialog::Accepted) { QStringList parts; if (mboxPtr) parts << mboxPtr->mailbox(); parts << ui.mailboxName->text(); if (ui.otherMailboxes->isChecked()) parts << QString(); QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString()); // FIXME: top-level separator imapModel()->createMailbox(targetName, ui.subscribe->isChecked() ? Imap::Mailbox::AutoSubscription::SUBSCRIBE : Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION ); } } void MainWindow::slotDeleteCurrentMailbox() { if (! mboxTree->currentIndex().isValid()) return; QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex()); Q_ASSERT(mailbox.isValid()); QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString(); if (QMessageBox::question(this, tr("Delete Mailbox"), tr("Are you sure to delete mailbox %1?").arg(name), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { imapModel()->deleteMailbox(name); } } void MainWindow::updateMessageFlags() { updateMessageFlagsOf(QModelIndex()); } void MainWindow::updateMessageFlagsOf(const QModelIndex &index) { QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection(); const bool isValid = !indexes.isEmpty() && // either we operate on the -already valided- selection or the index must be valid (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0); const bool okToModify = imapModel()->isNetworkAvailable() && isValid; markAsRead->setEnabled(okToModify); markAsDeleted->setEnabled(okToModify); markAsFlagged->setEnabled(okToModify); markAsJunk->setEnabled(okToModify); markAsNotJunk->setEnabled(okToModify); // There's no point in moving from Archive to, well, Archive auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); if (archiveFolderName.isEmpty()) { archiveFolderName = Common::SettingsNames::imapDefaultArchiveFolderName; } moveToArchive->setEnabled(okToModify && std::any_of(indexes.cbegin(), indexes.cend(), [archiveFolderName](const QModelIndex &i) { return i.data(Imap::Mailbox::RoleMailboxName) != archiveFolderName; })); tag1->setEnabled(okToModify); tag2->setEnabled(okToModify); tag3->setEnabled(okToModify); tag4->setEnabled(okToModify); tag5->setEnabled(okToModify); tag6->setEnabled(okToModify); tag7->setEnabled(okToModify); tag8->setEnabled(okToModify); tag9->setEnabled(okToModify); bool isRead = isValid, isDeleted = isValid, isFlagged = isValid, isJunk = isValid, isNotJunk = isValid, hasTag1 = isValid, hasTag2 = isValid, hasTag3 = isValid, hasTag4 = isValid, hasTag5 = isValid, hasTag6 = isValid, hasTag7 = isValid, hasTag8 = isValid, hasTag9 = isValid; auto updateTag = [=](const QModelIndex &i, bool &hasTag, int index) { if (hasTag && !m_favoriteTags->tagNameByIndex(index).isEmpty() && !i.data(Imap::Mailbox::RoleMessageFlags).toStringList().contains(m_favoriteTags->tagNameByIndex(index))) { hasTag = false; } }; Q_FOREACH (const QModelIndex &i, indexes) { #define UPDATE_STATE(PROP) \ if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \ is##PROP = false; UPDATE_STATE(Read) UPDATE_STATE(Deleted) UPDATE_STATE(Flagged) UPDATE_STATE(Junk) UPDATE_STATE(NotJunk) #undef UPDATE_STATE updateTag(i, hasTag1, 0); updateTag(i, hasTag2, 1); updateTag(i, hasTag3, 2); updateTag(i, hasTag4, 3); updateTag(i, hasTag5, 4); updateTag(i, hasTag6, 5); updateTag(i, hasTag7, 6); updateTag(i, hasTag8, 7); updateTag(i, hasTag9, 8); } markAsRead->setChecked(isRead); markAsDeleted->setChecked(isDeleted); markAsFlagged->setChecked(isFlagged); markAsJunk->setChecked(isJunk && !isNotJunk); markAsNotJunk->setChecked(isNotJunk && !isJunk); tag1->setChecked(hasTag1); tag2->setChecked(hasTag2); tag3->setChecked(hasTag3); tag4->setChecked(hasTag4); tag5->setChecked(hasTag5); tag6->setChecked(hasTag6); tag7->setChecked(hasTag7); tag8->setChecked(hasTag8); tag9->setChecked(hasTag9); } void MainWindow::updateActionsOnlineOffline(bool online) { reloadMboxList->setEnabled(online); resyncMbox->setEnabled(online); expunge->setEnabled(online); createChildMailbox->setEnabled(online); createTopMailbox->setEnabled(online); deleteCurrentMailbox->setEnabled(online); m_actionMarkMailboxAsRead->setEnabled(online); updateMessageFlags(); showImapCapabilities->setEnabled(online); if (!online) { m_replyGuess->setEnabled(false); m_replyPrivate->setEnabled(false); m_replyAll->setEnabled(false); m_replyAllButMe->setEnabled(false); m_replyList->setEnabled(false); m_forwardAsAttachment->setEnabled(false); m_bounce->setEnabled(false); } } void MainWindow::slotUpdateMessageActions() { Composer::RecipientList dummy; m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled() || m_replyAll->isEnabled() || m_replyList->isEnabled()); // Check the default reply mode // I suspect this is not going to work for everybody. Suggestions welcome... if (m_replyList->isEnabled()) { m_replyButton->setDefaultAction(m_replyList); } else if (m_replyAllButMe->isEnabled()) { m_replyButton->setDefaultAction(m_replyAllButMe); } else { m_replyButton->setDefaultAction(m_replyPrivate); } m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); m_bounce->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); } void MainWindow::scrollMessageUp() { m_messageWidget->area->ensureVisible(0, 0, 0, 0); } void MainWindow::slotReplyTo() { m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE); } void MainWindow::slotReplyAll() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL); } void MainWindow::slotReplyAllButMe() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME); } void MainWindow::slotReplyList() { m_messageWidget->messageView->reply(this, Composer::REPLY_LIST); } void MainWindow::slotReplyGuess() { if (m_replyButton->defaultAction() == m_replyAllButMe) { slotReplyAllButMe(); } else if (m_replyButton->defaultAction() == m_replyAll) { slotReplyAll(); } else if (m_replyButton->defaultAction() == m_replyList) { slotReplyList(); } else { slotReplyTo(); } } void MainWindow::slotForwardAsAttachment() { m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT); } void MainWindow::slotBounce() { QModelIndex index; Imap::Mailbox::Model::realTreeItem(msgListWidget->tree->currentIndex(), nullptr, &index); if (!index.isValid()) return; auto recipients = QList>(); for (const auto &kind: {Imap::Mailbox::RoleMessageTo, Imap::Mailbox::RoleMessageCc, Imap::Mailbox::RoleMessageBcc}) { for (const auto &oneAddr : index.data(kind).toList()) { Q_ASSERT(oneAddr.type() == QVariant::StringList); QStringList item = oneAddr.toStringList(); Q_ASSERT(item.size() == 4); Imap::Message::MailAddress a(item[0], item[1], item[2], item[3]); Composer::RecipientKind translatedKind = Composer::RecipientKind::ADDRESS_TO; switch (kind) { case Imap::Mailbox::RoleMessageTo: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_TO; break; case Imap::Mailbox::RoleMessageCc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_CC; break; case Imap::Mailbox::RoleMessageBcc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_BCC; break; default: Q_ASSERT(false); break; } recipients.push_back({translatedKind, a.asPrettyString()}); } } ComposeWidget::warnIfMsaNotConfigured( ComposeWidget::createFromReadOnly(this, index, recipients), this); } void MainWindow::slotComposeMailUrl(const QUrl &url) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this); } void MainWindow::slotManageContact(const QUrl &url) { Imap::Message::MailAddress addr; if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact"))) return; Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name); } void MainWindow::invokeContactEditor() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openAddressbookWindow(); } /** @short Create an MSAFactory as per the settings */ MSA::MSAFactory *MainWindow::msaFactory() { using namespace Common; QString method = m_settings->value(SettingsNames::msaMethodKey).toString(); MSA::MSAFactory *msaFactory = 0; if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) { msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(), m_settings->value(SettingsNames::smtpPortKey).toInt(), (method == SettingsNames::methodSSMTP), (method == SettingsNames::methodSMTP) && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(), m_settings->value(SettingsNames::smtpAuthKey).toBool(), m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ? m_settings->value(SettingsNames::imapUserKey).toString() : m_settings->value(SettingsNames::smtpUserKey).toString()); } else if (method == SettingsNames::methodSENDMAIL) { QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' ')); if (args.isEmpty()) { return 0; } QString appName = args.takeFirst(); msaFactory = new MSA::SendmailFactory(appName, args); } else if (method == SettingsNames::methodImapSendmail) { if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) { return 0; } msaFactory = new MSA::ImapSubmitFactory(qobject_cast(imapAccess()->imapModel())); } else { return 0; } return msaFactory; } void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't delete mailbox"), tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't create mailbox"), tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't open mailbox"), tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxChanged(const QModelIndex &mailbox) { using namespace Imap::Mailbox; QString mailboxName = mailbox.data(RoleMailboxName).toString(); bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() && m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() && mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString(); QTreeView *tree = msgListWidget->tree; // Automatically trigger visibility of the TO and FROM columns if (isSentMailbox) { if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) { tree->hideColumn(MsgListModel::FROM); tree->showColumn(MsgListModel::TO); } } else { if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) { tree->hideColumn(MsgListModel::TO); tree->showColumn(MsgListModel::FROM); } } updateMessageFlags(); slotScrollToUnseenMessage(); } void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state) { Q_UNUSED(parserId); static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE; QString message = connectionStateToString(state); if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) { // prevent excessive popups when we "reset the state" to something which is shown quite often showStatusMessage(QString()); } else { showStatusMessage(message); } previousState = state; } void MainWindow::slotShowLinkTarget(const QString &link) { if (link.isEmpty()) { QToolTip::hideText(); } else { QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link))); } } void MainWindow::slotShowAboutTrojita() { Ui::AboutDialog ui; QDialog *widget = new QDialog(this); widget->setAttribute(Qt::WA_DeleteOnClose); ui.setupUi(widget); ui.versionLabel->setText(Common::Application::version); ui.qtVersion->setText(QStringLiteral("Qt " QT_VERSION_STR "")); connect(ui.qtVersion, &QLabel::linkActivated, qApp, &QApplication::aboutQt); std::vector> features; features.emplace_back(tr("Plugins"), #ifdef WITH_SHARED_PLUGINS true #else false #endif ); features.emplace_back(tr("Encrypted and signed messages"), #ifdef TROJITA_HAVE_CRYPTO_MESSAGES true #else false #endif ); features.emplace_back(tr("IMAP compression"), #ifdef TROJITA_HAVE_ZLIB true #else false #endif ); QString featuresText = QStringLiteral("
    "); for (const auto x: features) { featuresText += x.second ? tr("
  • %1: supported
  • ").arg(x.first) : tr("
  • %1: disabled
  • ").arg(x.first); } featuresText += QStringLiteral("
"); ui.descriptionLabel->setText(ui.descriptionLabel->text() + featuresText); QStringList copyright; { // Find the names of the authors and remove date codes from there QFile license(QStringLiteral(":/LICENSE")); license.open(QFile::ReadOnly); const QString prefix(QStringLiteral("Copyright (C) ")); Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) { if (line.startsWith(prefix)) { const int pos = prefix.size(); copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') + line.mid(pos).replace(QRegExp(QLatin1String("(\\d) - (\\d)")), QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2")); } } } ui.credits->setTextFormat(Qt::PlainText); ui.credits->setText(copyright.join(QStringLiteral("\n"))); widget->show(); } void MainWindow::slotDonateToTrojita() { QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/")); } void MainWindow::slotSaveCurrentMessageBody() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this); netAccess->setModelMessage(messageIndex); Imap::Network::FileDownloadManager *fileDownloadManager = new Imap::Network::FileDownloadManager(this, netAccess, messageIndex); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &MainWindow::slotDownloadMessageFileNameRequested); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, this, &MainWindow::slotDownloadTransferError); connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater); fileDownloadManager->downloadMessage(); } } void MainWindow::slotDownloadTransferError(const QString &errorString) { Gui::Util::messageBoxCritical(this, tr("Can't save into file"), tr("Unable to save into file. Error:\n%1").arg(errorString)); } void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName) { *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0, QFileDialog::HideNameFilterDetails); } void MainWindow::slotViewMsgSource() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; auto w = messageSourceWidget(item); //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox. w->setWindowTitle(tr("Message source of UID %1 in %2").arg( QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()), Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString() )); w->show(); } } QWidget *MainWindow::messageSourceWidget(const QModelIndex &message) { QModelIndex messageIndex = Imap::deproxifiedIndex(message); MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex); sourceWidget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget); sourceWidget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, sourceWidget, &QWidget::close); return sourceWidget; } void MainWindow::slotViewMsgHeaders() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); auto widget = new MessageHeadersWidget(nullptr, messageIndex); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget); widget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, widget, &QWidget::close); widget->show(); } } #ifdef XTUPLE_CONNECT void MainWindow::slotXtSyncCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); QSettings s; QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList(); if (xtIncludeMailboxInSync->isChecked()) { if (! mailboxes.contains(mailbox)) { mailboxes.append(mailbox); } } else { mailboxes.removeAll(mailbox); } s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); prettyMboxModel->xtConnectStatusChanged(index); } #endif void MainWindow::slotSubscribeCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); if (m_actionSubscribeMailbox->isChecked()) { imapModel()->subscribeMailbox(mailbox); } else { imapModel()->unsubscribeMailbox(mailbox); } } void MainWindow::slotShowOnlySubscribed() { if (m_actionShowOnlySubscribed->isEnabled()) { m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked()); prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked()); } } void MainWindow::slotScrollToUnseenMessage() { // Now this is much, much more reliable than messing around with finding out an "interesting message"... if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) { // we're using some funky sorting, better don't scroll anywhere } if (m_actionSortDescending->isChecked()) { msgListWidget->tree->scrollToTop(); } else { msgListWidget->tree->scrollToBottom(); } } void MainWindow::slotScrollToCurrent() { // TODO: begs for lambda if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) { vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0 } } void MainWindow::slotThreadMsgList() { // We want to save user's preferences and not override them with "threading disabled" when the server // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here. const bool useThreading = actionThreadMsgList->isChecked(); // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make // sorting rather seldomly used as people shall instead use proper threading. if (useThreading) { m_actionSortThreading->setEnabled(true); if (!m_actionSortThreading->isChecked()) m_actionSortThreading->trigger(); m_actionSortNone->setEnabled(false); } else { m_actionSortNone->setEnabled(true); if (!m_actionSortNone->isChecked()) m_actionSortNone->trigger(); m_actionSortThreading->setEnabled(false); } QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex(); if (useThreading && actionThreadMsgList->isEnabled()) { msgListWidget->tree->setRootIsDecorated(true); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true); } else { msgListWidget->tree->setRootIsDecorated(false); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false); } m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading)); if (currentItem.isValid()) { msgListWidget->tree->scrollTo(currentItem); } else { // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same" // objects as what was visible before the reshuffling. slotScrollToUnseenMessage(); } } void MainWindow::slotSortingPreferenceChanged() { Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; using namespace Imap::Mailbox; int column = -1; if (m_actionSortByArrival->isChecked()) { column = MsgListModel::RECEIVED_DATE; } else if (m_actionSortByCc->isChecked()) { column = MsgListModel::CC; } else if (m_actionSortByDate->isChecked()) { column = MsgListModel::DATE; } else if (m_actionSortByFrom->isChecked()) { column = MsgListModel::FROM; } else if (m_actionSortBySize->isChecked()) { column = MsgListModel::SIZE; } else if (m_actionSortBySubject->isChecked()) { column = MsgListModel::SUBJECT; } else if (m_actionSortByTo->isChecked()) { column = MsgListModel::TO; } else { column = -1; } msgListWidget->tree->header()->setSortIndicator(column, order); } void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order) { // don't do anything during initialization if (!m_actionSortNone) return; using namespace Imap::Mailbox; QAction *action; switch (column) { case MsgListModel::SEEN: case MsgListModel::FLAGGED: case MsgListModel::ATTACHMENT: case MsgListModel::COLUMN_COUNT: case MsgListModel::BCC: case -1: if (actionThreadMsgList->isChecked()) action = m_actionSortThreading; else action = m_actionSortNone; break; case MsgListModel::SUBJECT: action = m_actionSortBySubject; break; case MsgListModel::FROM: action = m_actionSortByFrom; break; case MsgListModel::TO: action = m_actionSortByTo; break; case MsgListModel::CC: action = m_actionSortByCc; break; case MsgListModel::DATE: action = m_actionSortByDate; break; case MsgListModel::RECEIVED_DATE: action = m_actionSortByArrival; break; case MsgListModel::SIZE: action = m_actionSortBySize; break; default: action = m_actionSortNone; } action->setChecked(true); if (order == Qt::DescendingOrder) m_actionSortDescending->setChecked(true); else m_actionSortAscending->setChecked(true); } void MainWindow::slotSearchRequested(const QStringList &searchConditions) { if (!searchConditions.isEmpty() && actionThreadMsgList->isChecked()) { // right now, searching and threading doesn't play well together at all actionThreadMsgList->trigger(); } Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel = qobject_cast(m_imapAccess->threadingMsgListModel()); threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(), threadingMsgListModel->currentSortOrder()); } void MainWindow::slotHideRead() { const bool hideRead = actionHideRead->isChecked(); prettyMsgListModel->setHideRead(hideRead); m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead)); } void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities) { msgListWidget->tree->header()->viewport()->removeEventFilter(this); if (capabilities.contains(QStringLiteral("SORT"))) { m_actionSortByDate->actionGroup()->setEnabled(true); } else { m_actionSortByDate->actionGroup()->setEnabled(false); msgListWidget->tree->header()->viewport()->installEventFilter(this); } msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY"))); m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED"))); m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() && m_settings->value( Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool()); m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled()); const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities(); Q_FOREACH(const QString &capability, capabilities) { if (supportedCapabilities.contains(capability)) { actionThreadMsgList->setEnabled(true); if (actionThreadMsgList->isChecked()) slotThreadMsgList(); return; } } actionThreadMsgList->setEnabled(false); } void MainWindow::slotShowImapInfo() { QString caps; Q_FOREACH(const QString &cap, imapModel()->capabilities()) { caps += tr("
  • %1
  • \n").arg(cap); } QString idString; if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) { QMap serverId = imapModel()->serverId(); #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \ QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown"); IMAP_ID_FIELD(serverName, "name"); IMAP_ID_FIELD(serverVersion, "version"); IMAP_ID_FIELD(os, "os"); IMAP_ID_FIELD(osVersion, "os-version"); IMAP_ID_FIELD(vendor, "vendor"); IMAP_ID_FIELD(supportUrl, "support-url"); IMAP_ID_FIELD(address, "address"); IMAP_ID_FIELD(date, "date"); IMAP_ID_FIELD(command, "command"); IMAP_ID_FIELD(arguments, "arguments"); IMAP_ID_FIELD(environment, "environment"); #undef IMAP_ID_FIELD if (has_serverName) { idString = tr("

    "); if (has_serverVersion) idString += tr("Server: %1 %2").arg(serverName, serverVersion); else idString += tr("Server: %1").arg(serverName); if (has_vendor) { idString += tr(" (%1)").arg(vendor); } if (has_os) { if (has_osVersion) idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion); else idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os); } idString += tr("

    "); } else { idString = tr("

    The IMAP server did not return usable information about itself.

    "); } QString fullId; for (QMap::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) { fullId += tr("
  • %1: %2
  • ").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped()); } idString += tr("
      %1
    ").arg(fullId); } else { idString = tr("

    The server has not provided information about its software version.

    "); } QMessageBox::information(this, tr("IMAP Server Information"), tr("%1" "

    The following capabilities are currently advertised:

    \n" "
      \n%2
    ").arg(idString, caps)); } QSize MainWindow::sizeHint() const { return QSize(1150, 980); } void MainWindow::slotUpdateWindowTitle() { QModelIndex mailbox = qobject_cast(m_imapAccess->msgListModel())->currentMailbox(); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (!profileName.isEmpty()) profileName = QLatin1String(" [") + profileName + QLatin1Char(']'); if (mailbox.isValid()) { if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) { setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } else { setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } } else { setWindowTitle(trUtf8("Trojitá") + profileName); } } void MainWindow::slotLayoutCompact() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } if (!m_mainVSplitter) { m_mainVSplitter = new QSplitter(); m_mainVSplitter->setOrientation(Qt::Vertical); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainVSplitter->addWidget(msgListWidget); m_mainVSplitter->addWidget(m_messageWidget); m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(m_mainVSplitter); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainVSplitter->show(); m_mainHSplitter->show(); // The mboxTree shall not expand... m_mainHSplitter->setStretchFactor(0, 0); // ...while the msgListTree shall consume all the remaining space m_mainHSplitter->setStretchFactor(1, 1); // The CompleteMessageWidget shall not not collapse m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false); setCentralWidget(m_mainHSplitter); delete m_mainStack; m_layoutMode = LAYOUT_COMPACT; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact); applySizesAndState(); } void MainWindow::slotLayoutWide() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(msgListWidget); m_mainHSplitter->addWidget(m_messageWidget); msgListWidget->resize(mboxTree->size()); m_messageWidget->resize(mboxTree->size()); m_mainHSplitter->setStretchFactor(0, 0); m_mainHSplitter->setStretchFactor(1, 1); m_mainHSplitter->setStretchFactor(2, 1); m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainHSplitter->show(); setCentralWidget(m_mainHSplitter); delete m_mainStack; delete m_mainVSplitter; m_layoutMode = LAYOUT_WIDE; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide); applySizesAndState(); } void MainWindow::slotLayoutOneAtTime() { saveSizesAndState(); if (m_mainStack) return; m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack); setCentralWidget(m_mainStack); delete m_mainHSplitter; delete m_mainVSplitter; m_layoutMode = LAYOUT_ONE_AT_TIME; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime); applySizesAndState(); } Imap::Mailbox::Model *MainWindow::imapModel() const { return qobject_cast(m_imapAccess->imapModel()); } void MainWindow::desktopGeometryChanged() { m_delayedStateSaving->start(); } QString MainWindow::settingsKeyForLayout(const LayoutMode layout) { switch (layout) { case LAYOUT_COMPACT: return Common::SettingsNames::guiSizesInMainWinWhenCompact; case LAYOUT_WIDE: return Common::SettingsNames::guiSizesInMainWinWhenWide; case LAYOUT_ONE_AT_TIME: return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime; break; } return QString(); } void MainWindow::saveSizesAndState() { if (m_skipSavingOfUI) return; QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QList items; items << saveGeometry(); items << saveState(); items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray()); items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray()); items << msgListWidget->tree->header()->saveState(); items << QByteArray::number(msgListWidget->tree->header()->count()); for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) { items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i)); } // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number items << QByteArray::number(menuBar()->isVisible()); QByteArray buf; QDataStream stream(&buf, QIODevice::WriteOnly); stream << items.size(); Q_FOREACH(const QByteArray &item, items) { stream << item; } m_settings->setValue(key.arg(QString::number(geometry.width())), buf); } void MainWindow::saveRawStateSetting(bool enabled) { m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled); } void MainWindow::applySizesAndState() { QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray(); if (buf.isEmpty()) return; int size; QDataStream stream(&buf, QIODevice::ReadOnly); stream >> size; QByteArray item; // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to // save our geometries and state, which is what we must avoid while this method is executing. bool skipSaving = m_skipSavingOfUI; m_skipSavingOfUI = true; if (size-- && !stream.atEnd()) { stream >> item; // https://bugreports.qt-project.org/browse/QTBUG-30636 if (windowState() & Qt::WindowMaximized) { // restoreGeometry(.) restores the wrong size for at least maximized window // However, QWidget does also not notice that the configure request for this // is ignored by many window managers (because users really don't like when windows // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from // the wrong assumption the request would have been hononred. // So we just "fix" the internal geometry immediately afterwards to prevent // mislayouting // There's atm. no flicker due to this (and because Qt compresses events) // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true) QRect oldGeometry = MainWindow::geometry(); restoreGeometry(item); if (windowState() & Qt::WindowMaximized) setGeometry(oldGeometry); } else { restoreGeometry(item); if (windowState() & Qt::WindowMaximized) { // ensure to try setting the proper geometry and have the WM constrain us setGeometry(QApplication::desktop()->availableGeometry()); } } } if (size-- && !stream.atEnd()) { stream >> item; restoreState(item); } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainVSplitter) { m_mainVSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainHSplitter) { m_mainHSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; msgListWidget->tree->header()->restoreState(item); // got to manually update the state of the actions which control the visibility state msgListWidget->tree->updateActionsAfterRestoredState(); } connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns); if (size-- && !stream.atEnd()) { stream >> item; bool ok; int columns = item.toInt(&ok); if (ok) { msgListWidget->tree->header()->setStretchLastSection(false); for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) { stream >> item; int sectionSize = item.toInt(); QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i); if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) { // fun fact: user cannot resize by mouse when size <= 0 msgListWidget->tree->setColumnWidth(i, sectionSize); } else { msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i)); } msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode); } } } if (size-- && !stream.atEnd()) { stream >> item; bool ok; bool visibility = item.toInt(&ok); if (ok) { menuBar()->setVisible(visibility); showMenuBar->setChecked(visibility); } } m_skipSavingOfUI = skipSaving; } void MainWindow::resizeEvent(QResizeEvent *) { m_delayedStateSaving->start(); } /** @short Make sure that the message gets loaded after the splitters have changed their position */ void MainWindow::possiblyLoadMessageOnSplittersChanged() { if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) { // We do not have to check whether it's a different message; the setMessage() will do this or us // and there are multiple proxy models involved anyway QModelIndex index = msgListWidget->tree->currentIndex(); if (index.isValid()) { // OTOH, setting an invalid QModelIndex would happily assert-fail m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex()); } } } Imap::ImapAccess *MainWindow::imapAccess() const { return m_imapAccess; } void MainWindow::enableLoggingToDisk() { imapLogger->slotSetPersistentLogging(true); } void MainWindow::slotPluginsChanged() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow)) m_actionContactEditor->setEnabled(false); else m_actionContactEditor->setEnabled(true); } /** @short Update the default action to make sure that we show a correct status of the network connection */ void MainWindow::updateNetworkIndication() { if (QAction *action = qobject_cast(sender())) { if (action->isChecked()) { m_netToolbarDefaultAction->setIcon(action->icon()); } } } void MainWindow::showStatusMessage(const QString &message) { networkIndicator->setToolTip(message); if (isActiveWindow()) QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message); } void MainWindow::slotMessageModelChanged(QAbstractItemModel *model) { mailMimeTree->setModel(model); } void MainWindow::slotFavoriteTagsChanged() { for (int i = 1; i <= m_favoriteTags->rowCount(); ++i) { QAction *action = ShortcutHandler::instance()->action(QStringLiteral("action_tag_") + QString::number(i)); if (action) action->setText(tr("Tag with \"%1\"").arg(m_favoriteTags->tagNameByIndex(i - 1))); } } } diff --git a/src/Imap/Model/ItemRoles.h b/src/Imap/Model/ItemRoles.h index 3edf652c..a5475500 100644 --- a/src/Imap/Model/ItemRoles.h +++ b/src/Imap/Model/ItemRoles.h @@ -1,260 +1,262 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMAP_MODEL_ITEMROLES_H #define IMAP_MODEL_ITEMROLES_H #include namespace Imap { namespace Mailbox { /** @short Custom item data roles for IMAP */ enum { /** @short A "random" offset */ RoleBase = Qt::UserRole + 666, /** @short Is the item already fetched? */ RoleIsFetched, /** @short The item is not available -- perhaps we're offline and it isn't cached */ RoleIsUnavailable, /** @short Are we working in an offline mode? This role only works on a top-level item in a model. */ RoleIsNetworkOffline, /** @short Name of the mailbox */ RoleMailboxName, /** @short Short name of the mailbox */ RoleShortMailboxName, /** @short Separator for mailboxes at the current level */ RoleMailboxSeparator, /** @short Total number of messages in a mailbox */ RoleTotalMessageCount, /** @short Number of unread messages in a mailbox */ RoleUnreadMessageCount, /** @short Number of recent messages in a mailbox */ RoleRecentMessageCount, /** @short The mailbox in question is the INBOX */ RoleMailboxIsINBOX, /** @short The mailbox can be selected */ RoleMailboxIsSelectable, /** @short The mailbox has child mailboxes */ RoleMailboxHasChildMailboxes, /** @short Information about whether the number of messages in the mailbox has already been loaded */ RoleMailboxNumbersFetched, /** @short Is anything still loading for this mailbox? */ RoleMailboxItemsAreLoading, /** @short Current UIDVALIDITY of a mailbox */ RoleMailboxUidValidity, /** @short Is the mailbox subscribed? If the server doesn't support RFC5258, this can return wrong answer. */ RoleMailboxIsSubscribed, /** @short UID of the message */ RoleMessageUid, /** @short Subject of the message */ RoleMessageSubject, /** @short The From addresses */ RoleMessageFrom, /** @short The To addresses */ RoleMessageTo, /** @short The Cc addresses */ RoleMessageCc, /** @short The Bcc: addresses */ RoleMessageBcc, /** @short The Sender: header */ RoleMessageSender, /** @short The Reply-To: header */ RoleMessageReplyTo, /** @short The Message-Id: header */ RoleMessageMessageId, /** @short The In-Reply-To: header */ RoleMessageInReplyTo, /** @short The message timestamp as determined from IMAP's ENVELOPE, ie. from the RFC2822 headers */ RoleMessageDate, /** @short Timestamp of when the message was delivered to the mailbox (ie. IMAP's INTERNALDATE) */ RoleMessageInternalDate, /** @short Size of the message */ RoleMessageSize, /** @short Status of the \\Seen flag */ RoleMessageIsMarkedRead, /** @short Was unread when mailbox opened, or has been marked unread. This flag is transient and is recalculated * when switching mailboxes. */ RoleMessageWasUnread, /** @short Status of the \\Deleted flag */ RoleMessageIsMarkedDeleted, /** @short Was the message forwarded? */ RoleMessageIsMarkedForwarded, /** @short Was the message replied to? */ RoleMessageIsMarkedReplied, /** @short Is the message marked as a recent one? */ RoleMessageIsMarkedRecent, /** @short Is the message markes as flagged? */ RoleMessageIsMarkedFlagged, /** @short Is the message markes as junk? */ RoleMessageIsMarkedJunk, /** @short Is the message markes as notjunk? */ RoleMessageIsMarkedNotJunk, /** @short IMAP flags of a message */ RoleMessageFlags, /** @short Is the current item a root of thread with unread messages */ RoleThreadRootWithUnreadMessages, + /** @short Aggregated flags from the thread */ + RoleThreadAggregatedFlags, /** @short Fuzzy date of a particular message; useful for rough navigation */ RoleMessageFuzzyDate, /** @short List of message IDs from the message's References header */ RoleMessageHeaderReferences, /** @short The List-Post header from RFC 2369 */ RoleMessageHeaderListPost, /** @short Is the List-Post set to a special value of "NO"? */ RoleMessageHeaderListPostNo, /** @short A full message envelope */ RoleMessageEnvelope, /** @short Is this a mail with at least one attachment? The returned value might be a bit fuzzy. */ RoleMessageHasAttachments, /** @short Contents of a message part */ RolePartData, /** @short Unicode text, i.e. RolePartData already decoded */ RolePartUnicodeText, /** @short MIME type of a message part */ RolePartMimeType, /** @short Charset of a message part */ RolePartCharset, /** @short The format= parameter of the message part's Content-Type */ RolePartContentFormat, /** @short The delsp= parameter of the message part's Content-Type */ RolePartContentDelSp, /** @short Content-Transfer-Encoding of a message part */ RolePartTransferEncoding, /** @short The body-fld-id field from BODYSTRUCTURE */ RolePartBodyFldId, /** @short The Content-Disposition of a message part */ RolePartBodyDisposition, /** @short The file name for a message part */ RolePartFileName, /** @short The size of this part, as determined from BODYSTRUCTURE */ RolePartOctets, /** @short Access to the partId() function */ RolePartId, /** @short Access to the partToPath() function */ RolePartPathToPart, /** @short CID of the main part of a multipart/related message */ RolePartMultipartRelatedMainCid, /** @short Is this a top-level multipart, i.e. a multipart/... and a child of a message/rfc822? See isTopLevelMultipart. */ RolePartIsTopLevelMultipart, /** @short Return the body-fld-param from BODUSTRUCTURE, which usually contains some optional MIME parameters about this part */ RolePartBodyFldParam, /** @short Fetch a part from the cache if it's available, but do not request it from the server */ RolePartForceFetchFromCache, /** @short Pointer to the internal buffer */ RolePartBufferPtr, /** @short QModelIndex of the message a part is associated to */ RolePartMessageIndex, /** @short A relative IMAP URL pointing to this message or part, if available */ RoleIMAPRelativeUrl, /** @short Is the format of this particular multipart/signed supported for signature verification? A multipart/signed could use some unrecognized or unsupported algorithm, in which case we won't even try to verify the signature. If this is role returns true, it means that there will be just one child item and that that child's validity will be checked by the crypto operation. This role does not imply anything about the validity of the actual signature, though. */ RolePartSignatureVerifySupported, /** @short Is the format of this particular multipart/encrypted supported and recognized? See RolePartSignatureVerifySupported, this is an equivalent. */ RolePartDecryptionSupported, /** @short Is there any point in waiting longer? If true, this means that the crypto code is either waiting for data from the network, or that there is a crypto operation in progress. */ RolePartCryptoNotFinishedYet, /** @short Was there a failure in some cryptography operation which affected the ability to show the message? "Failure" means that something went wrong. Maybe some system component failed, or the message arrived too damaged to be decrypted. This state has nothing to do with, say, a message whose signature failed to verify. */ RolePartCryptoDecryptionFailed, /** @short Short message about the status/result of a crypto operation This is suitable for an immediate presentation to the user. The text should be short enough to not distract the user too much, but also descriptive enough to make sense on its own, without having to consult the longer, more detailed status message. */ RolePartCryptoTLDR, /** @short Longer information about the status/result of a crypto operation This can be shown to the user when they ask for more details. It could possibly be a very long text, including some cryptic output from gpg's stderr, for example. */ RolePartCryptoDetailedMessage, /** @short Icon name for showing the result of a crypto operation */ RolePartCryptoStatusIconName, /** @short Is this a valid signature subject to all checks, whatever they are? */ RolePartSignatureValidTrusted, /** @short Is this a technically valid signature without taking the trust level and other policies into account? */ RolePartSignatureValidDisregardingTrust, /** @short Who made the signature */ RolePartSignatureSignerName, /** @short When was the signature made */ RolePartSignatureSignDate, /** @short True if the item in the tasks list is actually a ParserState This role is *not* used or implemented by the IMAP models, but only by the TaskPresentationModel. */ RoleTaskIsParserState, /** @short True if the task shall be visible in the user-facing list of current activities This role is *not* used or implemented by the IMAP models, but only by the TaskPresentationModel and VisibleTasksModel. */ RoleTaskIsVisible, /** @short A short explanaiton of the task -- what is it doing? */ RoleTaskCompactName, /** @short Content-Disposition (inline or attachment) of an attachment within MessageComposer The enum value is converted to int. */ RoleAttachmentContentDispositionMode, /** @short The very last role */ RoleInvalidLastOne }; } } #endif // IMAP_MODEL_ITEMROLES_H diff --git a/src/Imap/Model/MailboxTree.h b/src/Imap/Model/MailboxTree.h index 6b9779c2..a72d4bbb 100644 --- a/src/Imap/Model/MailboxTree.h +++ b/src/Imap/Model/MailboxTree.h @@ -1,472 +1,473 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMAP_MAILBOXTREE_H #define IMAP_MAILBOXTREE_H #include #include #include #include #include #include "../Parser/Response.h" #include "../Parser/Message.h" #include "MailboxMetadata.h" namespace Imap { namespace Mailbox { class Model; class MailboxModel; class KeepMailboxOpenTask; class ListChildMailboxesTask; class TreeItem { friend class Model; // for m_loading and m_fetched TreeItem(const TreeItem &); // don't implement void operator=(const TreeItem &); // don't implement friend class DeleteMailboxTask; // for direct access to m_children friend class ObtainSynchronizedMailboxTask; friend class KeepMailboxOpenTask; // for direct access to m_children friend class ListChildMailboxesTask; // setStatus() in case of failure friend class MsgListModel; // for direct access to m_children friend class ThreadingMsgListModel; // for direct access to m_children friend class UpdateFlagsOfAllMessagesTask; // for direct access to m_children friend class FetchMsgPartTask; // for direct access to m_children protected: /** @short Availability of an item */ enum FetchingState { NONE, /**< @short No attempt to download an item has been made yet */ UNAVAILABLE, /**< @short Item isn't cached and remote requests are disabled */ LOADING, /**< @short Download of an item is already scheduled */ DONE /**< @short Item is available right now */ }; public: typedef enum { /** @short Full body of an e-mail stored on the IMAP server This one really makes sense on a TreeItemMessage and TreeItemPart, and are used */ /** @short The HEADER fetch modifier for the current item */ OFFSET_HEADER=1, /** @short The TEXT fetch modifier for the current item */ OFFSET_TEXT=2, /** @short The MIME fetch modifier for individual message parts In constrast to OFFSET_HEADER and OFFSET_TEXT, this one applies only to TreeItemPart, simply because using the MIME modifier on a top-level message is not allowed as per RFC 3501. */ OFFSET_MIME=3, /** @short Obtain the raw data without any kind of Content-Transfer-Encoding decoding */ OFFSET_RAW_CONTENTS = 4 } PartModifier; protected: static const intptr_t TagMask = 0x3; static const intptr_t PointerMask = ~TagMask; union { TreeItem *m_parent; intptr_t m_parentAsBits; }; TreeItemChildrenList m_children; FetchingState accessFetchStatus() const { return static_cast(m_parentAsBits & TagMask); } void setFetchStatus(const FetchingState fetchStatus) { m_parentAsBits = reinterpret_cast(parent()) | fetchStatus; } public: explicit TreeItem(TreeItem *parent); TreeItem *parent() const { return reinterpret_cast(m_parentAsBits & PointerMask); } virtual int row() const; virtual ~TreeItem(); virtual unsigned int childrenCount(Model *const model); virtual TreeItem *child(const int offset, Model *const model); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetch(Model *const model) = 0; virtual unsigned int rowCount(Model *const model) = 0; virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role) = 0; virtual bool hasChildren(Model *const model) = 0; virtual bool fetched() const { return accessFetchStatus() == DONE; } virtual bool loading() const { return accessFetchStatus() == LOADING; } virtual bool isUnavailable() const; virtual TreeItem *specialColumnPtr(int row, int column) const; virtual QModelIndex toIndex(Model *const model) const; }; class TreeItemPart; class TreeItemMessage; class TreeItemMailbox: public TreeItem { void operator=(const TreeItem &); // don't implement MailboxMetadata m_metadata; friend class Model; // needs access to maintianingTask friend class MailboxModel; friend class DeleteMailboxTask; // for direct access to maintainingTask friend class KeepMailboxOpenTask; // needs access to maintainingTask friend class SubscribeUnsubscribeTask; // needs access to m_metadata.flags friend class FetchMsgPartTask; // needs access to partIdToPtr() static QLatin1String flagNoInferiors; static QLatin1String flagHasNoChildren; static QLatin1String flagHasChildren; public: explicit TreeItemMailbox(TreeItem *parent); TreeItemMailbox(TreeItem *parent, Responses::List); ~TreeItemMailbox(); static TreeItemMailbox *fromMetadata(TreeItem *parent, const MailboxMetadata &metadata); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetch(Model *const model); virtual void fetchWithCacheControl(Model *const model, bool forceReload); virtual unsigned int rowCount(Model *const model); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); virtual TreeItem *child(const int offset, Model *const model); SyncState syncState; /** @short Returns true if this mailbox has child mailboxes This function might access the network if the answer can't be decided, for example on basis of mailbox flags. */ bool hasChildMailboxes(Model *const model); /** @short Return true if the mailbox is already known to not have any child mailboxes No network activity will be caused. If the answer is not known for sure, we return false (meaning "don't know"). */ bool hasNoChildMailboxesAlreadyKnown(); QString mailbox() const { return m_metadata.mailbox; } QString separator() const { return m_metadata.separator; } const MailboxMetadata &mailboxMetadata() const { return m_metadata; } /** @short Update internal tree with the results of a FETCH response If \a changedPart is not null, it will be updated to point to the message part whose content got fetched. */ void handleFetchResponse(Model *const model, const Responses::Fetch &response, QList &changedParts, TreeItemMessage *&changedMessage, bool usingQresync); void rescanForChildMailboxes(Model *const model); void handleExpunge(Model *const model, const Responses::NumberResponse &resp); void handleExists(Model *const model, const Responses::NumberResponse &resp); void handleVanished(Model *const model, const Responses::Vanished &resp); bool isSelectable() const; void saveSyncStateAndUids(Model *model); private: TreeItemPart *partIdToPtr(Model *model, TreeItemMessage *message, const QByteArray &msgId); /** @short ImapTask which is currently responsible for well-being of this mailbox */ QPointer maintainingTask; }; class TreeItemMsgList: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; friend class TreeItemMessage; // for maintaining the m_unreadMessageCount friend class Model; friend class ObtainSynchronizedMailboxTask; friend class KeepMailboxOpenTask; FetchingState m_numberFetchingStatus; int m_totalMessageCount; int m_unreadMessageCount; int m_recentMessageCount; public: explicit TreeItemMsgList(TreeItem *parent); virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); int totalMessageCount(Model *const model); int unreadMessageCount(Model *const model); int recentMessageCount(Model *const model); void fetchNumbers(Model *const model); void recalcVariousMessageCounts(Model *model); void recalcVariousMessageCountsOnExpunge(Model *model, TreeItemMessage *expungedMessage); void resetWasUnreadState(); bool numbersFetched() const; }; class MessageDataPayload { public: MessageDataPayload(); const Message::Envelope &envelope() const; void setEnvelope(const Message::Envelope &envelope); const QDateTime &internalDate() const; void setInternalDate(const QDateTime &internalDate); quint64 size() const; void setSize(const quint64 size); const QList &hdrReferences() const; void setHdrReferences(const QList &hdrReferences); const QList &hdrListPost() const; void setHdrListPost(const QList &hdrListPost); bool hdrListPostNo() const; void setHdrListPostNo(const bool hdrListPostNo); const QByteArray &rememberedBodyStructure() const; void setRememberedBodyStructure(const QByteArray &blob); TreeItemPart *partHeader() const; void setPartHeader(std::unique_ptr part); TreeItemPart *partText() const; void setPartText(std::unique_ptr part); bool isComplete() const; bool gotEnvelope() const; bool gotInternalDate() const; bool gotSize() const; bool gotHdrReferences() const; bool gotHdrListPost() const; bool gotRemeberedBodyStructure() const; private: Message::Envelope m_envelope; QDateTime m_internalDate; quint64 m_size; QList m_hdrReferences; QList m_hdrListPost; QByteArray m_rememberedBodyStructure; bool m_hdrListPostNo; std::unique_ptr m_partHeader; std::unique_ptr m_partText; bool m_gotEnvelope : 1; bool m_gotInternalDate : 1; bool m_gotSize : 1; bool m_gotBodystructure : 1; bool m_gotHdrReferences : 1; bool m_gotHdrListPost : 1; }; class TreeItemMessage: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; friend class TreeItemMsgList; friend class Model; friend class ObtainSynchronizedMailboxTask; // needs access to m_offset friend class KeepMailboxOpenTask; // needs access to m_offset + friend class ThreadingMsgListModel; // needs access to m_flags friend class UpdateFlagsTask; // needs access to m_flags friend class UpdateFlagsOfAllMessagesTask; // needs access to m_flags int m_offset; uint m_uid; mutable MessageDataPayload *m_data; QStringList m_flags; bool m_flagsHandled; bool m_wasUnread; /** @short Set FLAGS and maintain the unread message counter */ void setFlags(TreeItemMsgList *list, const QStringList &flags); void processAdditionalHeaders(Model *model, const QByteArray &rawHeaders); static bool hasNestedAttachments(Model *const model, TreeItemPart *part); MessageDataPayload *data() const { return m_data ? m_data : (m_data = new MessageDataPayload()); } public: explicit TreeItemMessage(TreeItem *parent); ~TreeItemMessage(); virtual int row() const; virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model) { Q_UNUSED(model); return true; } virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); Message::Envelope envelope(Model *const model); QDateTime internalDate(Model *const model); quint64 size(Model *const model); bool isMarkedAsDeleted() const; bool isMarkedAsRead() const; bool isMarkedAsReplied() const; bool isMarkedAsForwarded() const; bool isMarkedAsRecent() const; bool isMarkedAsFlagged() const; bool isMarkedAsJunk() const; bool isMarkedAsNotJunk() const; void checkFlagsReadRecent(bool &isRead, bool &isRecent) const; uint uid() const; virtual TreeItem *specialColumnPtr(int row, int column) const; bool hasAttachments(Model *const model); static QVariantList addresListToQVariant(const QList &addressList); }; class TreeItemPart: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; // needs access to m_data friend class Model; // dtto friend class FetchMsgPartTask; // needs m_binaryCTEFailed QByteArray m_mimeType; QByteArray m_charset; QByteArray m_contentFormat; QByteArray m_delSp; QByteArray m_transferEncoding; QByteArray m_data; QByteArray m_bodyFldId; QByteArray m_bodyDisposition; QString m_fileName; quint64 m_octets; QByteArray m_multipartRelatedStartPart; Imap::Message::AbstractMessage::bodyFldParam_t m_bodyFldParam; mutable TreeItemPart *m_partMime; mutable TreeItemPart *m_partRaw; bool m_binaryCTEFailed; public: TreeItemPart(TreeItem *parent, const QByteArray &mimeType); ~TreeItemPart(); virtual unsigned int childrenCount(Model *const model); virtual TreeItem *child(const int offset, Model *const model); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetchFromCache(Model *const model); virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); virtual QByteArray partId() const; /** @short Shall we use RFC3516 BINARY for fetching message parts or not */ typedef enum { /** @short Use the baseline IMAP feature, the BODY[...], from RFC 3501 */ FETCH_PART_IMAP, /** @short Fetch via the RFC3516's BINARY extension */ FETCH_PART_BINARY } PartFetchingMode; virtual QByteArray partIdForFetch(const PartFetchingMode fetchingMode) const; virtual QByteArray pathToPart() const; TreeItemMessage *message() const; /** @short Provide access to the internal buffer holding data It is safe to access the obtained pointer as long as this object is not deleted. This function violates the classic concept of object encapsulation, but is really useful for the implementation of Imap::Network::MsgPartNetworkReply. */ QByteArray *dataPtr(); QByteArray mimeType() const { return m_mimeType; } QByteArray charset() const { return m_charset; } void setCharset(const QByteArray &ch) { m_charset = ch; } void setContentFormat(const QByteArray &format) { m_contentFormat = format; } void setContentDelSp(const QByteArray &delSp) { m_delSp = delSp; } void setTransferEncoding(const QByteArray &transferEncoding) { m_transferEncoding = transferEncoding; } QByteArray transferEncoding() const { return m_transferEncoding; } void setBodyFldId(const QByteArray &id) { m_bodyFldId = id; } QByteArray bodyFldId() const { return m_bodyFldId; } void setBodyDisposition(const QByteArray &disposition) { m_bodyDisposition = disposition; } QByteArray bodyDisposition() const { return m_bodyDisposition; } void setFileName(const QString &name) { m_fileName = name; } QString fileName() const { return m_fileName; } void setOctets(const quint64 size) { m_octets = size; } /** @short Return the downloadable size of the message part */ quint64 octets() const { return m_octets; } QByteArray multipartRelatedStartPart() const { return m_multipartRelatedStartPart; } void setMultipartRelatedStartPart(const QByteArray &start) { m_multipartRelatedStartPart = start; } void setBodyFldParam(const Imap::Message::AbstractMessage::bodyFldParam_t &bodyFldParam) { m_bodyFldParam = bodyFldParam; } Imap::Message::AbstractMessage::bodyFldParam_t bodyFldParam() const { return m_bodyFldParam; } virtual TreeItem *specialColumnPtr(int row, int column) const; virtual bool isTopLevelMultiPart() const; virtual void silentlyReleaseMemoryRecursive(); protected: TreeItemPart(TreeItem *parent); }; /** @short A message part with a modifier This item hanldes fetching of message parts with an attached modifier (like TEXT, HEADER or MIME). */ class TreeItemModifiedPart: public TreeItemPart { PartModifier m_modifier; public: TreeItemModifiedPart(TreeItem *parent, const PartModifier kind); virtual int row() const; virtual unsigned int columnCount(); virtual QByteArray partId() const; virtual QByteArray pathToPart() const; virtual TreeItem *specialColumnPtr(int row, int column) const; PartModifier kind() const; virtual QModelIndex toIndex(Model *const model) const; virtual QByteArray partIdForFetch(const PartFetchingMode fetchingMode) const; protected: virtual bool isTopLevelMultiPart() const; private: QByteArray modifierToByteArray() const; }; /** @short Specialization of TreeItemPart for parts holding a multipart/message */ class TreeItemPartMultipartMessage: public TreeItemPart { Message::Envelope m_envelope; mutable std::unique_ptr m_partHeader; mutable std::unique_ptr m_partText; public: TreeItemPartMultipartMessage(TreeItem *parent, const Message::Envelope &envelope); virtual ~TreeItemPartMultipartMessage(); virtual QVariant data(Model * const model, int role); virtual TreeItem *specialColumnPtr(int row, int column) const; virtual void silentlyReleaseMemoryRecursive(); }; } } Q_DECLARE_METATYPE(QByteArray*) #endif // IMAP_MAILBOXTREE_H diff --git a/src/Imap/Model/PrettyMsgListModel.cpp b/src/Imap/Model/PrettyMsgListModel.cpp index 41cbd9c0..23c3537d 100644 --- a/src/Imap/Model/PrettyMsgListModel.cpp +++ b/src/Imap/Model/PrettyMsgListModel.cpp @@ -1,292 +1,279 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#include #include #include "PrettyMsgListModel.h" #include "ItemRoles.h" #include "MsgListModel.h" #include "ThreadingMsgListModel.h" #include "UiUtils/Formatting.h" #include "UiUtils/IconLoader.h" namespace Imap { namespace Mailbox { -PrettyMsgListModel::PrettyMsgListModel(QObject *parent, FavoriteTagsModel *m_favoriteTagsModel): - QSortFilterProxyModel(parent), m_favoriteTagsModel(m_favoriteTagsModel), m_hideRead(false) +PrettyMsgListModel::PrettyMsgListModel(QObject *parent): + QSortFilterProxyModel(parent), m_hideRead(false) { setDynamicSortFilter(true); } QVariant PrettyMsgListModel::data(const QModelIndex &index, int role) const { if (! index.isValid() || index.model() != this) return QVariant(); if (index.column() < 0 || index.column() >= columnCount(index.parent())) return QVariant(); if (index.row() < 0 || index.row() >= rowCount(index.parent())) return QVariant(); QModelIndex translated = mapToSource(index); switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: switch (index.column()) { case MsgListModel::TO: case MsgListModel::FROM: case MsgListModel::CC: case MsgListModel::BCC: { int backendRole = 0; switch (index.column()) { case MsgListModel::FROM: backendRole = RoleMessageFrom; break; case MsgListModel::TO: backendRole = RoleMessageTo; break; case MsgListModel::CC: backendRole = RoleMessageCc; break; case MsgListModel::BCC: backendRole = RoleMessageBcc; break; } QVariantList items = translated.data(backendRole).toList(); if (role == Qt::DisplayRole) { return Imap::Message::MailAddress::prettyList(items, Imap::Message::MailAddress::FORMAT_JUST_NAME); } else { return UiUtils::Formatting::htmlEscaped(Imap::Message::MailAddress::prettyList(items, Imap::Message::MailAddress::FORMAT_READABLE)); } } case MsgListModel::DATE: case MsgListModel::RECEIVED_DATE: { QDateTime res = translated.data(RoleMessageDate).toDateTime(); if (role == Qt::ToolTipRole) { // tooltips shall always show the full and complete data return res.toLocalTime().toString(Qt::DefaultLocaleLongDate); } return UiUtils::Formatting::prettyDate(res.toLocalTime()); } case MsgListModel::SIZE: { QVariant size = translated.data(RoleMessageSize); if (!size.isValid()) { return QVariant(); } return UiUtils::Formatting::prettySize(size.toULongLong()); } case MsgListModel::SUBJECT: { if (!translated.data(RoleIsFetched).toBool()) return tr("Loading..."); QString subject = translated.data(RoleMessageSubject).toString(); if (role == Qt::ToolTipRole) { subject = UiUtils::Formatting::htmlEscaped(subject); } return subject.isEmpty() ? tr("(no subject)") : subject; } } break; case Qt::TextAlignmentRole: switch (index.column()) { case MsgListModel::SIZE: return Qt::AlignRight; default: return QVariant(); } case Qt::DecorationRole: // We will need the data, but asking for Flags or IsMarkedXYZ doesn't cause a fetch translated.data(RoleMessageSubject); switch (index.column()) { case MsgListModel::SUBJECT: { if (! translated.data(RoleIsFetched).toBool()) return QVariant(); bool isForwarded = translated.data(RoleMessageIsMarkedForwarded).toBool(); bool isReplied = translated.data(RoleMessageIsMarkedReplied).toBool(); if (isForwarded && isReplied) return UiUtils::loadIcon(QStringLiteral("mail-forwarded-replied")); else if (isReplied) return UiUtils::loadIcon(QStringLiteral("mail-replied")); else if (isForwarded) return UiUtils::loadIcon(QStringLiteral("mail-forwarded")); else if (translated.data(RoleMessageIsMarkedRecent).toBool()) return UiUtils::loadIcon(QStringLiteral("mail-mark-unread-new")); else if (!translated.data(RoleMessageIsMarkedRead).toBool()) return UiUtils::loadIcon(QStringLiteral("mail-mark-unread")); else return UiUtils::loadIcon(QStringLiteral("mail-mark-read")); } case MsgListModel::SEEN: if (! translated.data(RoleIsFetched).toBool()) return QVariant(); break; case MsgListModel::FLAGGED: if (! translated.data(RoleIsFetched).toBool()) return QVariant(); if (translated.data(RoleMessageIsMarkedFlagged).toBool()) return UiUtils::loadIcon(QStringLiteral("mail-flagged")); else return UiUtils::loadIcon(QStringLiteral("mail-unflagged")); case MsgListModel::ATTACHMENT: if (translated.data(RoleMessageHasAttachments).toBool()) return UiUtils::loadIcon(QStringLiteral("mail-attachment")); else return QVariant(); default: return QVariant(); } case Qt::FontRole: { // We will need the data, but asking for Flags or IsMarkedXYZ doesn't cause a fetch translated.data(RoleMessageSubject); // These items should definitely *not* be rendered in bold if (!translated.data(RoleIsFetched).toBool()) return QVariant(); QFont font; if (translated.data(RoleMessageIsMarkedDeleted).toBool()) font.setStrikeOut(true); if (! translated.data(RoleMessageIsMarkedRead).toBool()) { // If any message is marked as unread, show it in bold and be done with it font.setBold(true); } else if (translated.model()->hasChildren(translated.sibling(translated.row(), 0)) && translated.data(RoleThreadRootWithUnreadMessages).toBool()) { // If this node is not marked as read, is a root of some thread and that thread // contains unread messages, display the thread's root underlined font.setUnderline(true); } if (index.column() == MsgListModel::SUBJECT && translated.data(RoleMessageSubject).toString().isEmpty()) { font.setItalic(true); } return font; } - case Qt::ForegroundRole: { - // These items should definitely *not* be rendered in color - if (!translated.data(RoleIsFetched).toBool()) - return QVariant(); - - QVariant messageFlags = translated.data(RoleMessageFlags); - auto color = m_favoriteTagsModel->findBestColorForTags(messageFlags.toStringList()); - if (color.isValid()) - return QBrush(color); - return QVariant(); - } - } return QSortFilterProxyModel::data(index, role); } void PrettyMsgListModel::setHideRead(bool value) { m_hideRead = value; invalidateFilter(); } bool PrettyMsgListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (!m_hideRead) return true; QModelIndex source_index = sourceModel()->index(source_row, 0, source_parent); for (QModelIndex test = source_index; test.isValid(); test = test.parent()) if (test.data(RoleThreadRootWithUnreadMessages).toBool() || test.data(RoleMessageWasUnread).toBool()) return true; return false; } void PrettyMsgListModel::sort(int column, Qt::SortOrder order) { ThreadingMsgListModel *threadingModel = qobject_cast(sourceModel()); Q_ASSERT(threadingModel); ThreadingMsgListModel::SortCriterium criterium = ThreadingMsgListModel::SORT_NONE; switch (column) { case MsgListModel::SEEN: case MsgListModel::FLAGGED: case MsgListModel::ATTACHMENT: case MsgListModel::COLUMN_COUNT: case MsgListModel::BCC: case -1: criterium = ThreadingMsgListModel::SORT_NONE; break; case MsgListModel::SUBJECT: criterium = ThreadingMsgListModel::SORT_SUBJECT; break; case MsgListModel::FROM: criterium = ThreadingMsgListModel::SORT_FROM; break; case MsgListModel::TO: criterium = ThreadingMsgListModel::SORT_TO; break; case MsgListModel::CC: criterium = ThreadingMsgListModel::SORT_CC; break; case MsgListModel::DATE: criterium = ThreadingMsgListModel::SORT_DATE; break; case MsgListModel::RECEIVED_DATE: criterium = ThreadingMsgListModel::SORT_ARRIVAL; break; case MsgListModel::SIZE: criterium = ThreadingMsgListModel::SORT_SIZE; break; } bool willSort = threadingModel->setUserSearchingSortingPreference(threadingModel->currentSearchCondition(), criterium, order); // Now let the view know about whether we accept such a sorting criteria. // This is needed because the QHeaderView doesn't offer a way to say "hey, you cannot sort in columns XYZ, only on ABC". if (criterium != ThreadingMsgListModel::SORT_NONE && willSort) emit sortingPreferenceChanged(column, order); else emit sortingPreferenceChanged(-1, order); } } } diff --git a/src/Imap/Model/PrettyMsgListModel.h b/src/Imap/Model/PrettyMsgListModel.h index bbe471ec..e3f9edd1 100644 --- a/src/Imap/Model/PrettyMsgListModel.h +++ b/src/Imap/Model/PrettyMsgListModel.h @@ -1,59 +1,57 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PRETTYMSGLISTMODEL_H #define PRETTYMSGLISTMODEL_H #include #include "Imap/Model/MailboxModel.h" #include "Imap/Model/FavoriteTagsModel.h" namespace Imap { namespace Mailbox { /** @short A pretty proxy model which increases sexiness of the (Threaded)MsgListModel */ class PrettyMsgListModel: public QSortFilterProxyModel { Q_OBJECT public: - explicit PrettyMsgListModel(QObject *parent, FavoriteTagsModel *m_favoriteTagsModel); + explicit PrettyMsgListModel(QObject *parent); virtual QVariant data(const QModelIndex &index, int role) const; void setHideRead(bool value); virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; virtual void sort(int column, Qt::SortOrder order); - FavoriteTagsModel *m_favoriteTagsModel; - signals: void sortingPreferenceChanged(int column, Qt::SortOrder order); private: bool m_hideRead; }; } } #endif // PRETTYMSGLISTMODEL_H diff --git a/src/Imap/Model/ThreadingMsgListModel.cpp b/src/Imap/Model/ThreadingMsgListModel.cpp index 7ea141ce..13380f39 100644 --- a/src/Imap/Model/ThreadingMsgListModel.cpp +++ b/src/Imap/Model/ThreadingMsgListModel.cpp @@ -1,1457 +1,1496 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ThreadingMsgListModel.h" #include #include #include #include "Imap/Tasks/SortTask.h" #include "Imap/Tasks/ThreadTask.h" #include "ItemRoles.h" #include "MailboxTree.h" #include "MsgListModel.h" namespace { /** @short Preallocate a bit more space in the hashmaps for future new arrivals */ const int headroomForNewmessages = 1000; } #if 0 namespace { using Imap::Mailbox::ThreadNodeInfo; QByteArray dumpThreadNodeInfo(const QHash &mapping, const uint nodeId, const uint offset) { QByteArray res; QByteArray prefix(offset, ' '); QTextStream ss(&res); Q_ASSERT(mapping.contains(nodeId)); const ThreadNodeInfo &node = mapping[nodeId]; ss << prefix << "ThreadNodeInfo intId " << node.internalId << " UID " << node.uid << " ptr " << node.ptr << " parentIntId " << node.parent << "\n"; Q_FOREACH(const uint childId, node.children) { ss << dumpThreadNodeInfo(mapping, childId, offset + 1); } return res; } } #endif namespace Imap { namespace Mailbox { ThreadingMsgListModel::ThreadingMsgListModel(QObject *parent): QAbstractProxyModel(parent), threadingHelperLastId(0), modelResetInProgress(false), threadingInFlight(false), m_shallBeThreading(false), m_sortTask(0), m_sortReverse(false), m_currentSortingCriteria(SORT_NONE), m_searchValidity(RESULT_INVALIDATED) { m_delayedPrune = new QTimer(this); m_delayedPrune->setSingleShot(true); m_delayedPrune->setInterval(0); connect(m_delayedPrune, &QTimer::timeout, this, &ThreadingMsgListModel::delayedPrune); } void ThreadingMsgListModel::setSourceModel(QAbstractItemModel *sourceModel) { beginResetModel(); threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); m_currentSortResult.clear(); m_searchValidity = RESULT_INVALIDATED; if (this->sourceModel()) { // there's already something, so take care to disconnect all signals this->sourceModel()->disconnect(this); } endResetModel(); if (!sourceModel) return; Imap::Mailbox::MsgListModel *msgList = qobject_cast(sourceModel); Q_ASSERT(msgList); QAbstractProxyModel::setSourceModel(msgList); // FIXME: will need to be expanded when Model supports more signals... connect(sourceModel, &QAbstractItemModel::modelReset, this, &ThreadingMsgListModel::resetMe); connect(sourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged); connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged); connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ThreadingMsgListModel::handleDataChanged); connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ThreadingMsgListModel::handleRowsAboutToBeRemoved); connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ThreadingMsgListModel::handleRowsRemoved); connect(sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &ThreadingMsgListModel::handleRowsAboutToBeInserted); connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ThreadingMsgListModel::handleRowsInserted); resetMe(); } QVariant ThreadingMsgListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (sourceModel()) { return sourceModel()->headerData(section, orientation, role); } else { return QVariant(); } } void ThreadingMsgListModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { // We don't support updates which concern multiple rows at this time. // Doing that would very likely require a completely different codepath due to threading... Q_ASSERT(topLeft.parent() == bottomRight.parent()); Q_ASSERT(topLeft.row() == bottomRight.row()); QModelIndex translated = mapFromSource(topLeft); emit dataChanged(translated, translated.sibling(translated.row(), bottomRight.column())); // We provide funny data like "does this thread contain unread messages?". Now the original signal might mean that flags of a // nested message have changed. In order to always be consistent, we have to find the thread root and emit dataChanged() on that // as well. QModelIndex rootCandidate = translated; while (rootCandidate.parent().isValid()) { rootCandidate = rootCandidate.parent(); } if (rootCandidate != translated) { // We're really an embedded message emit dataChanged(rootCandidate, rootCandidate.sibling(rootCandidate.row(), bottomRight.column())); } auto message = dynamic_cast(static_cast(topLeft.internalPointer())); Q_ASSERT(message); if (message->uid() == 0) { // UID is not yet known. // This is a legal situation, for example when an unsolicited FETCH FLAGS arrives and there's no UID in there. return; } QSet::iterator persistent = unknownUids.find(message); if (persistent != unknownUids.end()) { // The message wasn't fully synced before, and now it is persistent = unknownUids.erase(persistent); if (unknownUids.isEmpty()) { wantThreading(); } } } QModelIndex ThreadingMsgListModel::index(int row, int column, const QModelIndex &parent) const { Q_ASSERT(!parent.isValid() || parent.model() == this); if (threading.isEmpty()) { // mapping not available yet return QModelIndex(); } if (row < 0 || column < 0 || column >= MsgListModel::COLUMN_COUNT) return QModelIndex(); if (parent.isValid() && parent.column() != 0) { // only the first column should have children return QModelIndex(); } uint parentId = parent.isValid() ? parent.internalId() : 0; QHash::const_iterator it = threading.constFind(parentId); Q_ASSERT(it != threading.constEnd()); if (it->children.size() <= row) return QModelIndex(); return createIndex(row, column, it->children[row]); } QModelIndex ThreadingMsgListModel::parent(const QModelIndex &index) const { if (! index.isValid() || index.model() != this) return QModelIndex(); if (threading.isEmpty()) return QModelIndex(); if (index.row() < 0 || index.column() < 0 || index.column() >= MsgListModel::COLUMN_COUNT) return QModelIndex(); QHash::const_iterator node = threading.constFind(index.internalId()); if (node == threading.constEnd()) return QModelIndex(); QHash::const_iterator parentNode = threading.constFind(node->parent); Q_ASSERT(parentNode != threading.constEnd()); Q_ASSERT(parentNode->internalId == node->parent); if (parentNode->internalId == 0) return QModelIndex(); return createIndex(parentNode->offset, 0, parentNode->internalId); } bool ThreadingMsgListModel::hasChildren(const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return false; return ! threading.isEmpty() && ! threading.value(parent.internalId()).children.isEmpty(); } int ThreadingMsgListModel::rowCount(const QModelIndex &parent) const { if (threading.isEmpty()) return 0; if (parent.isValid() && parent.column() != 0) return 0; return threading.value(parent.internalId()).children.size(); } int ThreadingMsgListModel::columnCount(const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return 0; return MsgListModel::COLUMN_COUNT; } QModelIndex ThreadingMsgListModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid() || !proxyIndex.internalId()) return QModelIndex(); if (threading.isEmpty()) return QModelIndex(); Imap::Mailbox::MsgListModel *msgList = qobject_cast(sourceModel()); Q_ASSERT(msgList); QHash::const_iterator node = threading.constFind(proxyIndex.internalId()); if (node == threading.constEnd()) return QModelIndex(); if (node->ptr) { return msgList->createIndex(node->ptr->row(), proxyIndex.column(), node->ptr); } else { // it's a fake message return QModelIndex(); } } QModelIndex ThreadingMsgListModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) return QModelIndex(); Q_ASSERT(sourceIndex.model() == sourceModel()); QHash::const_iterator it = ptrToInternal.constFind(sourceIndex.internalPointer()); if (it == ptrToInternal.constEnd()) return QModelIndex(); const uint internalId = *it; QHash::const_iterator node = threading.constFind(internalId); if (node == threading.constEnd()) { // The filtering criteria say that this index shall not be visible return QModelIndex(); } Q_ASSERT(node != threading.constEnd()); return createIndex(node->offset, sourceIndex.column(), internalId); } QVariant ThreadingMsgListModel::data(const QModelIndex &proxyIndex, int role) const { if (! proxyIndex.isValid() || proxyIndex.model() != this) return QVariant(); QHash::const_iterator it = threading.constFind(proxyIndex.internalId()); Q_ASSERT(it != threading.constEnd()); if (it->ptr) { // It's a real item which exists in the underlying model - if (role == RoleThreadRootWithUnreadMessages) { + switch (role) { + case RoleThreadRootWithUnreadMessages: if (proxyIndex.parent().isValid()) { // We don't support this kind of questions for other messages than the roots of the threads. // Other components, like the QML bindings, are however happy to request that, so let's just return // a reasonable result instead of whinning about callers requesting useless stuff. return false; } else { return threadContainsUnreadMessages(it->internalId); } - } else { + case RoleThreadAggregatedFlags: + return threadAggregatedFlags(it->internalId); + default: return QAbstractProxyModel::data(proxyIndex, role); } } switch (role) { case Qt::DisplayRole: if (proxyIndex.column() == 0) return tr("[Message is missing]"); break; case Qt::ToolTipRole: return tr("This thread refers to an extra message, but that message is not present in the " "selected mailbox, or is missing from the current search context."); } return QVariant(); } Qt::ItemFlags ThreadingMsgListModel::flags(const QModelIndex &index) const { if (! index.isValid() || index.model() != this) return Qt::NoItemFlags; QHash::const_iterator it = threading.constFind(index.internalId()); Q_ASSERT(it != threading.constEnd()); if (it->ptr && it->uid) return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled; return Qt::NoItemFlags; } void ThreadingMsgListModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); for (int i = start; i <= end; ++i) { QModelIndex index = sourceModel()->index(i, 0, parent); Q_ASSERT(index.isValid()); QModelIndex translated = mapFromSource(index); unknownUids.remove(static_cast(index.internalPointer())); if (!translated.isValid()) { // The index being removed wasn't visible in our mapping anyway continue; } Q_ASSERT(translated.isValid()); QHash::iterator it = threading.find(translated.internalId()); Q_ASSERT(it != threading.end()); it->uid = 0; it->ptr = 0; } } void ThreadingMsgListModel::handleRowsRemoved(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); Q_UNUSED(start); Q_UNUSED(end); if (!m_delayedPrune->isActive()) m_delayedPrune->start(); } void ThreadingMsgListModel::delayedPrune() { emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); pruneTree(); updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); int myStart = threading[0].children.size(); int myEnd = myStart + (end - start); beginInsertRows(QModelIndex(), myStart, myEnd); } void ThreadingMsgListModel::handleRowsInserted(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); for (int i = start; i <= end; ++i) { QModelIndex index = sourceModel()->index(i, 0); uint uid = index.data(RoleMessageUid).toUInt(); ThreadNodeInfo node; node.internalId = ++threadingHelperLastId; node.uid = uid; node.ptr = static_cast(index.internalPointer()); node.offset = threading[0].children.size(); threading[node.internalId] = node; threading[0].children << node.internalId; ptrToInternal[node.ptr] = node.internalId; if (!node.uid) { unknownUids << static_cast(index.internalPointer()); } else { threadedRootIds.append(node.internalId); } } endInsertRows(); if (!m_sortTask || !m_sortTask->isPersistent()) { m_currentSortResult.clear(); if (m_searchValidity == RESULT_FRESH) m_searchValidity = RESULT_INVALIDATED; } if (m_shallBeThreading) wantThreading(); } void ThreadingMsgListModel::resetMe() { // Prevent possible recursion here if (modelResetInProgress) return; beginResetModel(); modelResetInProgress = true; threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); m_currentSortResult.clear(); m_searchValidity = RESULT_INVALIDATED; endResetModel(); updateNoThreading(); modelResetInProgress = false; // Refresh the sorting/searching/threading preferences. // This is important, otherwise we won't track threading and/or the sort direction after e.g. changing a mailbox. wantThreading(); } void ThreadingMsgListModel::updateNoThreading() { threadingHelperLastId = 0; if (!sourceModel()) { // Maybe we got reset because the parent model is no longer here... if (! threading.isEmpty()) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); threading.clear(); ptrToInternal.clear(); endRemoveRows(); } unknownUids.clear(); return; } emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); int upstreamMessages = sourceModel()->rowCount(); QList allIds; QHash newThreading; QHash newPtrToInternal; if (upstreamMessages) { // Prefer the direct pointer access instead of going through the MVC API -- similar to how applyThreading() works. // This improves the speed of the testSortingPerformance benchmark by 18%. QModelIndex firstMessageIndex = sourceModel()->index(0, 0); Q_ASSERT(firstMessageIndex.isValid()); const Model *realModel = 0; TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel); Q_ASSERT(firstMessagePtr); // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer()); TreeItemMsgList *list = dynamic_cast(firstMessagePtr->parent()); Q_ASSERT(list); newThreading.reserve(upstreamMessages + headroomForNewmessages); newPtrToInternal.reserve(upstreamMessages + headroomForNewmessages); for (int i = 0; i < upstreamMessages; ++i) { TreeItemMessage *ptr = static_cast(list->m_children[i]); Q_ASSERT(ptr); ThreadNodeInfo node; node.internalId = i + 1; node.uid = ptr->uid(); node.ptr = ptr; node.offset = i; newThreading[node.internalId] = node; allIds.append(node.internalId); newPtrToInternal[node.ptr] = node.internalId; if (!node.uid) { unknownUids << ptr; } } } if (newThreading.size()) { threading = newThreading; ptrToInternal = newPtrToInternal; threading[ 0 ].children = allIds; threading[ 0 ].ptr = 0; threadingHelperLastId = newThreading.size(); threadedRootIds = threading[0].children; } updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::wantThreading(const SkipSortSearch skipSortSearch) { if (!sourceModel() || !sourceModel()->rowCount() || !m_shallBeThreading) { updateNoThreading(); if (skipSortSearch == AUTO_SORT_SEARCH) { searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } return; } if (threadingInFlight) { // Imagine the following scenario: // <<< "* 3 EXISTS" // Message 2 has unknown UID // >>> "y4 UID FETCH 66:* (FLAGS)" // >>> "y5 UID THREAD REFS utf-8 ALL" // <<< "* 3 FETCH (UID 66 FLAGS ())" // Got UID for seq# 3 // ThreadingMsgListModel::wantThreading: THREAD contains info about UID 1 (or higher), mailbox has 66 // *** this is the interesting part *** // <<< "y4 OK fetch" // <<< "* THREAD (1)(2)(66)" // <<< "y5 OK thread" // >>> "y6 UID THREAD REFS utf-8 ALL" // // See, at the indicated (***) place, we already have an in-flight THREAD request and receive UID for newly arrived // message. We certainly don't want to ask for threading once again; it's better to wait a bit and only ask when the // to-be-received THREAD does not contain all required UIDs. if (skipSortSearch == AUTO_SORT_SEARCH) { searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } return; } const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailbox = realIndex.parent().parent(); TreeItemMsgList *list = dynamic_cast(static_cast(realIndex.parent().internalPointer())); Q_ASSERT(list); // Something has happened and we want to process the THREAD response QVector mapping = realModel->cache()->messageThreading(mailbox.data(RoleMailboxName).toString()); // Find the UID of the last message in the mailbox uint highestUidInMailbox = findHighestUidInMailbox(list); uint highestUidInThreadingLowerBound = findHighEnoughNumber(mapping, highestUidInMailbox); logTrace(QStringLiteral("ThreadingMsgListModel::wantThreading: THREAD contains info about UID %1 (or higher), mailbox has %2") .arg(QString::number(highestUidInThreadingLowerBound), QString::number(highestUidInMailbox))); if (highestUidInThreadingLowerBound >= highestUidInMailbox) { // There's no point asking for data at this point, we shall just apply threading applyThreading(mapping); } else { // There's apparently at least one known UID whose threading info we do not know; that means that we have to ask the // server here. auto roughlyLastKnown = const_cast(realModel)->findMessageOrNextOneByUid(list, highestUidInThreadingLowerBound); if (list->m_children.end() - roughlyLastKnown >= 50 || roughlyLastKnown == list->m_children.begin()) { askForThreading(); } else { askForThreading(static_cast(*roughlyLastKnown)->uid() + 1); } } } uint ThreadingMsgListModel::findHighestUidInMailbox(TreeItemMsgList *list) { uint highestUidInMailbox = 0; for (int i = sourceModel()->rowCount() - 1; i > -1 && !highestUidInMailbox; --i) { highestUidInMailbox = dynamic_cast(list->m_children[i])->uid(); } return highestUidInMailbox; } uint ThreadingMsgListModel::findHighEnoughNumber(const QVector &mapping, uint marker) { if (mapping.isEmpty()) return 0; // Find the highest UID for which we have the threading info uint highestUidInThreadingLowerBound = 0; // If the threading already contains everything we need, we could have a higher chance of finding the high enough UID at the // end of the list. On the other hand, in case when the cached THREAD response does not cintain everything we need, we're out // of luck, we have absolutely no guarantee about relative greatness of parent/child/siblings in the tree. // Searching backward could lead to faster lookups, but we cannot avoid a full lookup in the bad case. for (int i = mapping.size() - 1; i >= 0; --i) { if (highestUidInThreadingLowerBound < mapping[i].num) { highestUidInThreadingLowerBound = mapping[i].num; if (highestUidInThreadingLowerBound >= marker) { // There's no point going further, we already know that we shall ask for threading return highestUidInThreadingLowerBound; } } // OK, we have to consult our children highestUidInThreadingLowerBound = qMax(highestUidInThreadingLowerBound, findHighEnoughNumber(mapping[i].children, marker)); if (highestUidInThreadingLowerBound >= marker) { return highestUidInThreadingLowerBound; } } return highestUidInThreadingLowerBound; } void ThreadingMsgListModel::askForThreading(const uint firstUnknownUid) { Q_ASSERT(m_shallBeThreading); Q_ASSERT(sourceModel()); Q_ASSERT(sourceModel()->rowCount()); const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); if (realModel->capabilities().contains(QStringLiteral("THREAD=REFS"))) { requestedAlgorithm = "REFS"; } else if (realModel->capabilities().contains(QStringLiteral("THREAD=REFERENCES"))) { requestedAlgorithm = "REFERENCES"; } else if (realModel->capabilities().contains(QStringLiteral("THREAD=ORDEREDSUBJECT"))) { requestedAlgorithm = "ORDEREDSUBJECT"; } if (! requestedAlgorithm.isEmpty()) { threadingInFlight = true; if (firstUnknownUid && realModel->capabilities().contains(QStringLiteral("INCTHREAD"))) { auto threadTask = realModel->m_taskFactory-> createIncrementalThreadTask(const_cast(realModel), mailboxIndex, requestedAlgorithm, QStringList() << QStringLiteral("INTHREAD") << QString::fromUtf8(requestedAlgorithm) << QStringLiteral("UID") << QString::fromUtf8(Sequence::startingAt(firstUnknownUid).toByteArray())); connect(threadTask, &ThreadTask::incrementalThreadingAvailable, this, &ThreadingMsgListModel::slotIncrementalThreadingAvailable); connect(threadTask, &ImapTask::failed, this, &ThreadingMsgListModel::slotIncrementalThreadingFailed); } else { realModel->m_taskFactory->createThreadTask(const_cast(realModel), mailboxIndex, requestedAlgorithm, QStringList() << QStringLiteral("ALL")); connect(realModel, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); connect(realModel, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); } } } /** @short Gather all UIDs present in the mapping and push them into the "uids" vector */ static void gatherAllUidsFromThreadNode(Imap::Uids &uids, const QVector &list) { for (QVector::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { uids.push_back(it->num); gatherAllUidsFromThreadNode(uids, it->children); } } void ThreadingMsgListModel::slotIncrementalThreadingAvailable(const Responses::ESearch::IncrementalThreadingData_t &data) { // Preparation: get through to the real model const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); Q_ASSERT(someMessage.isValid()); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); Q_ASSERT(mailboxIndex.isValid()); // First phase: remove all messages mentioned in the incremental responses from their original placement Imap::Uids affectedUids; for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) { gatherAllUidsFromThreadNode(affectedUids, it->thread); } qSort(affectedUids); QList affectedMessages = const_cast(realModel)-> findMessagesByUids(static_cast(mailboxIndex.internalPointer()), affectedUids); QHash uidToPtrCache; emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); for (QList::const_iterator it = affectedMessages.constBegin(); it != affectedMessages.constEnd(); ++it) { QHash::const_iterator ptrMappingIt = ptrToInternal.constFind(*it); Q_ASSERT(ptrMappingIt != ptrToInternal.constEnd()); QHash::iterator threadIt = threading.find(*ptrMappingIt); Q_ASSERT(threadIt != threading.end()); uidToPtrCache[(*it)->uid()] = threadIt->ptr; threadIt->ptr = 0; } pruneTree(); updatePersistentIndexesPhase2(); emit layoutChanged(); // Second phase: for each message whose UID is returned by the server, update the threading data QSet usedNodes; emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) { registerThreading(it->thread, 0, uidToPtrCache, usedNodes); int actualOffset = threading[0].children.size() - 1; int expectedOffsetOfPrevious = threading[0].children.indexOf(it->previousThreadRoot); if (actualOffset == expectedOffsetOfPrevious + 1) { // it's on the correct position, yay! } else { // move the new subthread to a correct place threading[0].children.insert(expectedOffsetOfPrevious + 1, threading[0].children.takeLast()); // push the rest (including the new arrival) forward for (int i = expectedOffsetOfPrevious + 1; i < threading[0].children.size(); ++i) { threading[threading[0].children[i]].offset = i; } } } updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::slotIncrementalThreadingFailed() { } bool ThreadingMsgListModel::shouldIgnoreThisThreadingResponse(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const Model **realModel) { QModelIndex someMessage = sourceModel()->index(0,0); if (!someMessage.isValid()) return true; const Model *model; QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &model, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); if (mailboxIndex != mailbox) { // this is for another mailbox return true; } if (algorithm != requestedAlgorithm) { logTrace(QStringLiteral("Weird, asked for threading via %1 but got %2 instead -- ignoring") .arg(QString::fromUtf8(requestedAlgorithm), QString::fromUtf8(algorithm))); return true; } if (searchCriteria.size() != 1 || searchCriteria.front() != QLatin1String("ALL")) { QString buf; QTextStream ss(&buf); logTrace(QStringLiteral("Weird, requesting messages matching ALL, but got this instead: %1") .arg(searchCriteria.join(QStringLiteral(", ")))); return true; } if (realModel) *realModel = model; return false; } void ThreadingMsgListModel::slotThreadingFailed(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria) { // Better safe than sorry -- prevent infinite waiting to the maximal possible extent threadingInFlight = false; if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria)) return; auto model = qobject_cast(sender()); Q_ASSERT(model); disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); updateNoThreading(); } void ThreadingMsgListModel::slotThreadingAvailable(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const QVector &mapping) { // Better safe than sorry -- prevent infinite waiting to the maximal possible extent threadingInFlight = false; const Model *model = 0; if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria, &model)) return; Q_ASSERT(model); disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); model->cache()->setMessageThreading(mailbox.data(RoleMailboxName).toString(), mapping); // Indirect processing here -- the wantThreading() will check that the received response really contains everything we need // and if it does, simply applyThreading() that. If there's something missing, it will ask for the threading again. if (m_shallBeThreading) wantThreading(); } void ThreadingMsgListModel::slotSortingAvailable(const Imap::Uids &uids) { if (!m_sortTask->isPersistent()) { disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_sortTask = 0; } m_currentSortResult = uids; if (m_searchValidity == RESULT_ASKED) m_searchValidity = RESULT_FRESH; wantThreading(); } void ThreadingMsgListModel::slotSortingFailed() { disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_sortTask = 0; m_sortReverse = false; calculateNullSort(); applySort(); emit sortingFailed(); } void ThreadingMsgListModel::slotSortingIncrementalUpdate(const Responses::ESearch::IncrementalContextData_t &updates) { for (Responses::ESearch::IncrementalContextData_t::const_iterator it = updates.constBegin(); it != updates.constEnd(); ++it) { switch (it->modification) { case Responses::ESearch::ContextIncrementalItem::ADDTO: for (int i = 0; i < it->uids.size(); ++i) { int offset; if (it->offset == 0) { // FIXME: use mailbox order later on offset = 0; } else { // IMAP uses one-based indexing, we use zero-based offsets offset = it->offset + i - 1; } if (offset < 0 || offset > m_currentSortResult.size()) { throw MailboxException("ESEARCH: ADDTO out of bounds"); } m_currentSortResult.insert(offset, it->uids[i]); } break; case Responses::ESearch::ContextIncrementalItem::REMOVEFROM: for (int i = 0; i < it->uids.size(); ++i) { if (it->offset == 0) { // When the offset is not given, we have to find it ourselves auto item = std::find(m_currentSortResult.begin(), m_currentSortResult.end(), it->uids[i]); if (item == m_currentSortResult.end()) { throw MailboxException("ESEARCH: there's no such UID"); } m_currentSortResult.erase(item); } else { // We're given an offset, so let's make sure it is a correct one int offset = it->offset + i - 1; if (offset < 0 || offset >= m_currentSortResult.size()) { throw MailboxException("ESEARCH: REMOVEFROM out of bounds"); } if (m_currentSortResult[offset] != it->uids[i]) { throw MailboxException("ESEARCH: REMOVEFROM UID mismatch"); } m_currentSortResult.remove(offset); } } break; } } m_searchValidity = RESULT_FRESH; wantThreading(); } /** @short Store UIDs of the thread roots as the "current search order" */ void ThreadingMsgListModel::calculateNullSort() { m_currentSortResult.clear(); m_currentSortResult.reserve(threadedRootIds.size() + headroomForNewmessages); Q_FOREACH(const uint internalId, threadedRootIds) { QHash::const_iterator it = threading.constFind(internalId); if (it == threading.constEnd()) continue; if (it->uid) m_currentSortResult.append(it->uid); } m_searchValidity = RESULT_FRESH; } void ThreadingMsgListModel::applyThreading(const QVector &mapping) { if (! unknownUids.isEmpty()) { // Some messages have UID zero, which means that they weren't loaded yet. Too bad. logTrace(QStringLiteral("%1 messages have 0 UID").arg(unknownUids.size())); return; } emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); threading.clear(); ptrToInternal.clear(); // Default-construct the root node threading[ 0 ].ptr = 0; // At first, initialize threading nodes for all messages which are right now available in the mailbox. // We risk that we will have to delete some of them later on, but this is likely better than doing a lookup // for each UID individually (remember, the THREAD response might contain UIDs in crazy order). int upstreamMessages = sourceModel()->rowCount(); QHash uidToPtrCache; QSet usedNodes; uidToPtrCache.reserve(upstreamMessages + headroomForNewmessages); threading.reserve(upstreamMessages + headroomForNewmessages); ptrToInternal.reserve(upstreamMessages + headroomForNewmessages); if (upstreamMessages) { // Work with pointers instead going through the MVC API for performance. // This matters (at least that's what by benchmarks said). QModelIndex firstMessageIndex = sourceModel()->index(0, 0); Q_ASSERT(firstMessageIndex.isValid()); const Model *realModel = 0; TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel); Q_ASSERT(firstMessagePtr); // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer()); TreeItemMsgList *list = dynamic_cast(firstMessagePtr->parent()); Q_ASSERT(list); for (int i = 0; i < upstreamMessages; ++i) { ThreadNodeInfo node; node.uid = dynamic_cast(list->m_children[i])->uid(); if (! node.uid) { throw UnknownMessageIndex("Encountered a message with zero UID when threading. This is a bug in Trojita, sorry."); } node.internalId = i + 1; node.ptr = list->m_children[i]; uidToPtrCache[node.uid] = node.ptr; threadingHelperLastId = node.internalId; // We're creating a new node here Q_ASSERT(!threading.contains(node.internalId)); threading[ node.internalId ] = node; ptrToInternal[ node.ptr ] = node.internalId; } } // Mark the root node as always present usedNodes.insert(0); // Set up parents and find the list of all used nodes registerThreading(mapping, 0, uidToPtrCache, usedNodes); // Now remove all messages which were not referenced in the THREAD response from our mapping QHash::iterator it = threading.begin(); while (it != threading.end()) { if (usedNodes.contains(it.key())) { // this message should be shown ++it; } else { // this message is not included in the list of messages actually to be shown ptrToInternal.remove(it->ptr); it = threading.erase(it); } } pruneTree(); updatePersistentIndexesPhase2(); if (rowCount()) threadedRootIds = threading[0].children; emit layoutChanged(); // If the sorting was active before, we shall reactivate it now searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } void ThreadingMsgListModel::registerThreading(const QVector &mapping, uint parentId, const QHash &uidToPtr, QSet &usedNodes) { Q_FOREACH(const Imap::Responses::ThreadingNode &node, mapping) { uint nodeId; QHash::const_iterator ptrIt; if (node.num == 0 || (ptrIt = uidToPtr.find(node.num)) == uidToPtr.constEnd()) { // Either this is an empty node, or the THREAD response references a UID which is no longer in the mailbox. // This is a valid scenario; it can happen e.g. when reusing data from cache, or when a message got // expunged after the untagged THREAD was received, but before the tagged OK. // We cannot just ignore this node, though, because it might have some children which we would otherwise // simply hide. // The ptrIt which is initialized by the condition is used in the else branch. ThreadNodeInfo fake; fake.internalId = ++threadingHelperLastId; fake.parent = parentId; Q_ASSERT(threading.contains(parentId)); // The child will be registered to the list of parent's children after the if/else branch threading[ fake.internalId ] = fake; nodeId = fake.internalId; } else { QHash::const_iterator nodeIt = ptrToInternal.constFind(*ptrIt); // The following assert would fail if there was a node with a valid UID, but not in our ptrToInternal mapping. // That is however non-issue, as we pre-create nodes for all messages beforehand. Q_ASSERT(nodeIt != ptrToInternal.constEnd()); nodeId = *nodeIt; // This is needed for the incremental stuff threading[nodeId].ptr = static_cast(*ptrIt); } threading[nodeId].offset = threading[parentId].children.size(); threading[ parentId ].children.append(nodeId); threading[ nodeId ].parent = parentId; usedNodes.insert(nodeId); registerThreading(node.children, nodeId, uidToPtr, usedNodes); } } /** @short Gather a list of persistent indexes which we have to transform after out layout change */ void ThreadingMsgListModel::updatePersistentIndexesPhase1() { oldPersistentIndexes = persistentIndexList(); oldPtrs.clear(); Q_FOREACH(const QModelIndex &idx, oldPersistentIndexes) { // the index could get invalidated by the pruneTree() or something else manipulating our threading bool isOk = idx.isValid() && threading.contains(idx.internalId()); if (!isOk) { oldPtrs << 0; continue; } QModelIndex translated = mapToSource(idx); if (!translated.isValid()) { // another stale item oldPtrs << 0; continue; } oldPtrs << translated.internalPointer(); } } /** @short Update the gathered persistent indexes after our change in the layout */ void ThreadingMsgListModel::updatePersistentIndexesPhase2() { Q_ASSERT(oldPersistentIndexes.size() == oldPtrs.size()); QList updatedIndexes; for (int i = 0; i < oldPersistentIndexes.size(); ++i) { QHash::const_iterator ptrIt = ptrToInternal.constFind(oldPtrs[i]); if (ptrIt == ptrToInternal.constEnd()) { // That message is no longer there updatedIndexes.append(QModelIndex()); continue; } QHash::const_iterator it = threading.constFind(*ptrIt); if (it == threading.constEnd()) { // Filtering doesn't accept this index, let's declare it dead updatedIndexes.append(QModelIndex()); } else { updatedIndexes.append(createIndex(it->offset, oldPersistentIndexes[i].column(), it->internalId)); } } Q_ASSERT(oldPersistentIndexes.size() == updatedIndexes.size()); changePersistentIndexList(oldPersistentIndexes, updatedIndexes); oldPersistentIndexes.clear(); oldPtrs.clear(); } void ThreadingMsgListModel::pruneTree() { // Our mapping (threading) is completely unsorted, which means that we simply don't have any way of walking the tree from // the top. Instead, we got to work with a random walk, processing nodes in an unspecified order. If we iterated on the QHash // directly, we'd hit an issue with iterator ordering (basically, we want to be able to say "hey, I don't care at which point // of the iteration I'm right now, the next node to process should be that one, and then we should resume with the rest"). QList pending = threading.keys(); // These are the parents whose children will have to be renumbered later on QSet parentsForRenumbering; for (QList::iterator id = pending.begin(); id != pending.end(); /* nothing */) { // Convert to the hashmap // The "it" iterator point to the current node in the threading mapping QHash::iterator it = threading.find(*id); if (it == threading.end()) { // We've already seen this node, that's due to promoting ++id; continue; } if (it->internalId == 0) { // A special root item; we should not delete that one :) ++id; continue; } if (it->ptr) { // regular and valid message -> skip ++id; } else { // a fake one // each node has a parent QHash::iterator parent = threading.find(it->parent); Q_ASSERT(parent != threading.end()); // and the node itself has to be found in its parent's children QList::iterator childIt = qFind(parent->children.begin(), parent->children.end(), it->internalId); Q_ASSERT(childIt != parent->children.end()); // The offset of this child might no longer be correct, though -- we're postponing the actual deletion until later if (it->children.isEmpty()) { // This is a leaf node, so we can just remove it childIt = parent->children.erase(childIt); // We do not perform the renumbering immediately, that would lead to an O(n^2) performance when deleting nodes. parentsForRenumbering.insert(it->parent); parentsForRenumbering.remove(it->internalId); if (it->parent == 0) { threadedRootIds.removeOne(it->internalId); } threading.erase(it); ++id; } else { // This node has some children, so we can't just delete it. Instead of that, we promote its first child // to replace this node. QHash::iterator replaceWith = threading.find(it->children.first()); Q_ASSERT(replaceWith != threading.end()); // The offsets will, again, be updated later on parentsForRenumbering.insert(it->parent); parentsForRenumbering.insert(replaceWith.key()); parentsForRenumbering.remove(it->internalId); // Replace the node *childIt = it->children.first(); replaceWith->parent = parent->internalId; // Now merge the lists of children it->children.removeFirst(); replaceWith->children = replaceWith->children + it->children; // Fix parent information of all children of the replacement node for (int i = 0; i < replaceWith->children.size(); ++i) { QHash::iterator sibling = threading.find(replaceWith->children[i]); Q_ASSERT(sibling != threading.end()); sibling->parent = replaceWith.key(); } if (parent->internalId == 0) { // Update the list of all thread roots QList::iterator rootIt = qFind(threadedRootIds.begin(), threadedRootIds.end(), it->internalId); if (rootIt != threadedRootIds.end()) *rootIt = replaceWith->internalId; } // Now that all references are gone, remove the original node threading.erase(it); if (!replaceWith->ptr) { // If the just-promoted item is also a fake one, we'll have to visit it as well. This assignment is safe, // because we've already processed the current item and are completely done with it. The worst which can // happen is that we'll visit the same node twice, which is reasonably acceptable. *id = replaceWith.key(); } } } } // Now fix the sequential numbering of all siblings of deleted children Q_FOREACH(const auto parentId, parentsForRenumbering) { auto parentIt = threading.constFind(parentId); Q_ASSERT(parentIt != threading.constEnd()); int offset = 0; for (auto childNumber = parentIt->children.constBegin(); childNumber != parentIt->children.constEnd(); ++childNumber, ++offset) { auto childIt = threading.find(*childNumber); Q_ASSERT(childIt != threading.end()); childIt->offset = offset; } } } QStringList ThreadingMsgListModel::supportedCapabilities() { return QStringList() << QStringLiteral("THREAD=REFS") << QStringLiteral("THREAD=REFERENCES") << QStringLiteral("THREAD=ORDEREDSUBJECT"); } QStringList ThreadingMsgListModel::mimeTypes() const { return sourceModel() ? sourceModel()->mimeTypes() : QStringList(); } QMimeData *ThreadingMsgListModel::mimeData(const QModelIndexList &indexes) const { if (! sourceModel()) return 0; QModelIndexList translated; Q_FOREACH(const QModelIndex &idx, indexes) { translated << mapToSource(idx); } return sourceModel()->mimeData(translated); } -bool ThreadingMsgListModel::threadContainsUnreadMessages(const uint root) const +template +bool threadForeachCallback(std::function callback, const TreeItemMessage &message) +{ + callback(message); + return false; +} +template<> +bool threadForeachCallback(std::function callback, const TreeItemMessage &message) +{ + return callback(message); +} + +/** @short Execute the provided function once for each message + +Returns immediately if the provided function returns `true`. +*/ +template +void ThreadingMsgListModel::threadForeach(const uint &root, std::function callback) const { - // FIXME: cache the value somewhere... QList queue; queue.append(root); while (! queue.isEmpty()) { uint current = queue.takeFirst(); QHash::const_iterator it = threading.constFind(current); Q_ASSERT(it != threading.constEnd()); if (it->ptr) { // Because of the delayed delete via pruneTree, we can hit a null pointer here TreeItemMessage *message = dynamic_cast(it->ptr); Q_ASSERT(message); - if (! message->isMarkedAsRead()) - return true; + if (threadForeachCallback(callback, *message)) + return; } queue.append(it->children); } - return false; +} + +bool ThreadingMsgListModel::threadContainsUnreadMessages(const uint root) const +{ + // FIXME: cache the value somewhere... + bool containsUnreadMessages = false; + threadForeach(root, [&containsUnreadMessages](const TreeItemMessage &message) -> const bool { + return containsUnreadMessages = ! message.isMarkedAsRead(); + }); + return containsUnreadMessages; +} + +QStringList ThreadingMsgListModel::threadAggregatedFlags(const uint root) const +{ + // FIXME: cache the value somewhere... + QStringList aggregatedFlags; + threadForeach(root, [&aggregatedFlags](const TreeItemMessage &message) { + aggregatedFlags += message.m_flags; + }); + aggregatedFlags.removeDuplicates(); + return aggregatedFlags; } /** @short Pass a debugging message to the real Model, if possible If we don't know what the real model is, just dump it through the qDebug(); that's better than nothing. */ void ThreadingMsgListModel::logTrace(const QString &message) { if (!sourceModel()) { qDebug() << message; return; } QModelIndex idx = sourceModel()->index(0, 0); if (!idx.isValid()) { qDebug() << message; return; } // Got to find out the real model and also translate the index to one belonging to a real Model Q_ASSERT(idx.model()); const Model *realModel; QModelIndex realIndex; Model::realTreeItem(idx, &realModel, &realIndex); Q_ASSERT(realModel); QModelIndex mailboxIndex = const_cast(realModel)->findMailboxForItems(QModelIndexList() << realIndex); const_cast(realModel)->logTrace(mailboxIndex, Common::LOG_OTHER, QStringLiteral("ThreadingMsgListModel for %1").arg(mailboxIndex.data(RoleMailboxName).toString()), message); } void ThreadingMsgListModel::setUserWantsThreading(bool enable) { m_shallBeThreading = enable; if (m_shallBeThreading) { wantThreading(); } else { updateNoThreading(); } } bool ThreadingMsgListModel::setUserSearchingSortingPreference(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order) { wantThreading(SKIP_SORT_SEARCH); return searchSortPreferenceImplementation(searchConditions, criterium, order); } /** @short The workhorse behind setUserSearchingSortingPreference() */ bool ThreadingMsgListModel::searchSortPreferenceImplementation(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order) { Q_ASSERT(sourceModel()); m_sortReverse = order == Qt::DescendingOrder; if (!sourceModel()->rowCount()) { return false; } const Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); bool hasDisplaySort = false; bool hasSort = false; if (realModel->capabilities().contains(QStringLiteral("SORT=DISPLAY"))) { hasDisplaySort = true; hasSort = true; } else if (realModel->capabilities().contains(QStringLiteral("SORT"))) { // just the regular sort hasSort = true; } QStringList sortOptions; switch (criterium) { case SORT_ARRIVAL: sortOptions << QStringLiteral("ARRIVAL"); break; case SORT_CC: sortOptions << QStringLiteral("CC"); break; case SORT_DATE: sortOptions << QStringLiteral("DATE"); break; case SORT_FROM: sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYFROM") : QStringLiteral("FROM")); break; case SORT_SIZE: sortOptions << QStringLiteral("SIZE"); break; case SORT_SUBJECT: sortOptions << QStringLiteral("SUBJECT"); break; case SORT_TO: sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYTO") : QStringLiteral("TO")); break; case SORT_NONE: if (m_sortTask && m_sortTask->isPersistent() && (m_currentSearchConditions != searchConditions || m_currentSortingCriteria != criterium)) { // Any change shall result in us killing that sort task m_sortTask->cancelSortingUpdates(); } m_currentSortingCriteria = criterium; if (searchConditions.isEmpty()) { // This operation is special, it will immediately restore the original shape of the mailbox m_currentSearchConditions = searchConditions; calculateNullSort(); applySort(); return true; } else if (searchConditions != m_currentSearchConditions || m_searchValidity != RESULT_FRESH) { // We have to update our search conditions m_sortTask = realModel->m_taskFactory->createSortTask(const_cast(realModel), mailboxIndex, searchConditions, QStringList()); connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_currentSearchConditions = searchConditions; m_searchValidity = RESULT_ASKED; } else { // A result of SEARCH has just arrived Q_ASSERT(m_searchValidity == RESULT_FRESH); applySort(); } return true; } if (!hasSort) { // sorting is completely unsupported return false; } Q_ASSERT(!sortOptions.isEmpty()); if (m_currentSortingCriteria == criterium && m_currentSearchConditions == searchConditions && m_searchValidity != RESULT_INVALIDATED) { applySort(); } else { m_currentSearchConditions = searchConditions; m_currentSortingCriteria = criterium; calculateNullSort(); applySort(); if (m_sortTask && m_sortTask->isPersistent()) m_sortTask->cancelSortingUpdates(); m_sortTask = realModel->m_taskFactory->createSortTask(const_cast(realModel), mailboxIndex, searchConditions, sortOptions); connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_searchValidity = RESULT_ASKED; } return true; } void ThreadingMsgListModel::applySort() { if (!sourceModel()->rowCount()) { // empty mailbox is a corner case and it's already sorted anyway return; } const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Model::realTreeItem(someMessage, &realModel, &realIndex); TreeItemMailbox *mailbox = dynamic_cast(static_cast(realIndex.parent().parent().internalPointer())); Q_ASSERT(mailbox); emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); QSet newlyUnreachable(threading[0].children.toSet()); threading[0].children.clear(); threading[0].children.reserve(m_currentSortResult.size() + headroomForNewmessages); QSet allRootIds(threadedRootIds.toSet()); for (int i = 0; i < m_currentSortResult.size(); ++i) { int offset = m_sortReverse ? m_currentSortResult.size() - 1 - i : i; QList messages = const_cast(realModel) ->findMessagesByUids(mailbox, Imap::Uids() << m_currentSortResult[offset]); if (messages.isEmpty()) { // wrong UID, weird continue; } Q_ASSERT(messages.size() == 1); QHash::const_iterator it = ptrToInternal.constFind(messages.front()); Q_ASSERT(it != ptrToInternal.constEnd()); if (!allRootIds.contains(*it)) { // not a thread root, so don't show it continue; } threading[*it].offset = threading[0].children.size(); threading[0].children.append(*it); } // Now remove everything which is no longer reachable from the root of the thread mapping // Start working on the top-level orphans Q_FOREACH(const uint uid, threading[0].children) { newlyUnreachable.remove(uid); } std::vector queue(newlyUnreachable.constBegin(), newlyUnreachable.constEnd()); for (std::vector::size_type i = 0; i < queue.size(); ++i) { QHash::iterator threadingIt = threading.find(queue[i]); Q_ASSERT(threadingIt != threading.end()); queue.insert(queue.end(), threadingIt->children.constBegin(), threadingIt->children.constEnd()); threading.erase(threadingIt); } updatePersistentIndexesPhase2(); emit layoutChanged(); } QStringList ThreadingMsgListModel::currentSearchCondition() const { return m_currentSearchConditions; } ThreadingMsgListModel::SortCriterium ThreadingMsgListModel::currentSortCriterium() const { return m_currentSortingCriteria; } Qt::SortOrder ThreadingMsgListModel::currentSortOrder() const { return m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder; } QModelIndex ThreadingMsgListModel::sibling(int row, int column, const QModelIndex &idx) const { return index(row, column, idx.parent()); } } } diff --git a/src/Imap/Model/ThreadingMsgListModel.h b/src/Imap/Model/ThreadingMsgListModel.h index bdb8baeb..c8f7d3dd 100644 --- a/src/Imap/Model/ThreadingMsgListModel.h +++ b/src/Imap/Model/ThreadingMsgListModel.h @@ -1,319 +1,327 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMAP_THREADINGMSGLISTMODEL_H #define IMAP_THREADINGMSGLISTMODEL_H +#include #include #include #include +#include "MailboxTree.h" #include "Imap/Parser/Response.h" class QTimer; class ImapModelThreadingTest; /** @short Namespace for IMAP interaction */ namespace Imap { /** @short Classes for handling of mailboxes and connections */ namespace Mailbox { class SortTask; class TreeItem; class TreeItemMsgList; /** @short A node in tree structure used for threading representation */ struct ThreadNodeInfo { /** @short Internal unique identifier used for model indexes */ uint internalId; /** @short A UID of the message in a mailbox */ uint uid; /** @short internalId of a parent of this message */ uint parent; /** @short List of children of current node */ QList children; /** @short Pointer to the TreeItemMessage* of the corresponding message */ TreeItem *ptr; /** @short Position among our parent's children */ int offset; ThreadNodeInfo(): internalId(0), uid(0), parent(0), ptr(0), offset(0) {} }; QDebug operator<<(QDebug debug, const ThreadNodeInfo &node); /** @short A model implementing view of the whole IMAP server The problem with threading is that due to the extremely asynchronous nature of the IMAP Model, we often get informed about indexes to messages which "just arrived", and therefore do not have even their UID available. That sucks, because we have to somehow handle them. Situation gets a bit more complicated by the initial syncing -- this ThreadingMsgListModel can't tell whether the rowsInserted() signals mean that the underlying model is getting populated, or whether it's a sign of a just-arrived message. On a plus side, the Model guarantees that the only occurrence when a message could have UID 0 is when the mailbox has been synced previously, and the message is a new arrival. In all other contexts (that is, during the mailbox re-synchronization), there is a hard guarantee that the UID of any message available via the MVC API will always be non-zero. The model should also refrain from sending extra THREAD commands to the server, and cache the responses locally. This is pretty easy for message deletions, as it should be only a matter of replacing some node in the threading info with a fake ThreadNodeInfo node and running the pruneTree() method, except that we might not know the UID of the message in question, and hence can't know what to delete. */ class ThreadingMsgListModel: public QAbstractProxyModel { Q_OBJECT Q_ENUMS(SortCriterium) public: /** @short On which column to sort The possible columns are described in RFC 5256, section 3. No support for multiple columns is present. Trojitá will automatically upgrade to the display-based search criteria from RFC 5957 if support for that RFC is indicated by the server. */ typedef enum { /** @short Don't do any explicit sorting If threading is not active, the order of messages represnets the order in which they appear in the IMAP mailbox. In case the display is threaded already, the order depends on the threading algorithm. */ SORT_NONE, /** @short RFC5256's ARRIVAL key, ie. the INTERNALDATE */ SORT_ARRIVAL, /** @short The Cc field from the IMAP ENVELOPE */ SORT_CC, /** @short Timestamp when the message was created, if available */ SORT_DATE, /** @short Either the display name or the mailbox of the "sender" of a message from the "From" header */ SORT_FROM, /** @short Size of the message */ SORT_SIZE, /** @short The subject of the e-mail */ SORT_SUBJECT, /** @short Recipient of the message, either their mailbox or their display name */ SORT_TO } SortCriterium; explicit ThreadingMsgListModel(QObject *parent); virtual void setSourceModel(QAbstractItemModel *sourceModel); virtual QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const; virtual QModelIndex parent(const QModelIndex &index) const; virtual int rowCount(const QModelIndex &parent=QModelIndex()) const; virtual int columnCount(const QModelIndex &parent=QModelIndex()) const; virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const; virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; virtual bool hasChildren(const QModelIndex &parent=QModelIndex()) const; virtual QVariant data(const QModelIndex &proxyIndex, int role) const; virtual Qt::ItemFlags flags(const QModelIndex &index) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; // Qt5 reimplements sibling() within the proxy models, and the default implementation constitutes // a behavior change compared to Qt4. virtual QModelIndex sibling(int row, int column, const QModelIndex &idx) const; virtual QStringList mimeTypes() const; virtual QMimeData *mimeData(const QModelIndexList &indexes) const; /** @short List of capabilities which could be used for threading If any of them are present in server's capabilities, at least some level of threading will be possible. */ static QStringList supportedCapabilities(); QStringList currentSearchCondition() const; SortCriterium currentSortCriterium() const; Q_INVOKABLE Qt::SortOrder currentSortOrder() const; public slots: void resetMe(); void handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); void handleRowsRemoved(const QModelIndex &parent, int start, int end); void handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); void handleRowsInserted(const QModelIndex &parent, int start, int end); /** @short Feed this with the data from a THREAD response */ void slotThreadingAvailable(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const QVector &mapping); void slotThreadingFailed(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria); /** @short Really apply threading to this model */ void applyThreading(const QVector &mapping); /** @short SORT response has arrived */ void slotSortingAvailable(const Imap::Uids &uids); /** @short SORT has failed */ void slotSortingFailed(); /** @short Dynamic update to the current SORT order */ void slotSortingIncrementalUpdate(const Imap::Responses::ESearch::IncrementalContextData_t &updates); void applySort(); /** @short Enable or disable threading */ void setUserWantsThreading(bool enable); Q_INVOKABLE bool setUserSearchingSortingPreference(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order = Qt::AscendingOrder); void slotIncrementalThreadingAvailable(const Responses::ESearch::IncrementalThreadingData_t &data); void slotIncrementalThreadingFailed(); void delayedPrune(); signals: void sortingFailed(); private: /** @short Display messages without any threading at all, as a liner list */ void updateNoThreading(); /** @short Ask the model for a THREAD response If the firstUnknownUid is different than zero, an incremental response is requested. */ void askForThreading(const uint firstUnknownUid = 0); void updatePersistentIndexesPhase1(); void updatePersistentIndexesPhase2(); /** @short Shall we ask for SORT/SEARCH automatically? */ typedef enum { AUTO_SORT_SEARCH, SKIP_SORT_SEARCH } SkipSortSearch; /** @short Apply cached THREAD response or ask for threading again */ void wantThreading(const SkipSortSearch skipSortSearch = AUTO_SORT_SEARCH); /** @short Convert the threading from a THREAD response and apply that threading to this model */ void registerThreading(const QVector &mapping, uint parentId, const QHash &uidToPtr, QSet &usedNodes); bool searchSortPreferenceImplementation(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order = Qt::AscendingOrder); /** @short Remove fake messages from the threading tree */ void pruneTree(); + /** @short Execute the provided function once for each message */ + template void threadForeach(const uint &root, std::function callback) const; + /** @short Check current thread for "unread messages" */ bool threadContainsUnreadMessages(const uint root) const; + /** @short Return aggregated flags from the thread */ + QStringList threadAggregatedFlags(const uint root) const; + /** @short Is this someone else's THREAD response? */ bool shouldIgnoreThisThreadingResponse(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const Model **realModel=0); /** @short Return some number from the thread mapping @arg mapping which is either the highest among them, or at least as high as the marker*/ static uint findHighEnoughNumber(const QVector &mapping, uint marker); void calculateNullSort(); uint findHighestUidInMailbox(TreeItemMsgList *list); void logTrace(const QString &message); ThreadingMsgListModel &operator=(const ThreadingMsgListModel &); // don't implement ThreadingMsgListModel(const ThreadingMsgListModel &); // don't implement /** @short Mapping from the upstream model's internalId to ThreadingMsgListModel's internal IDs */ QHash ptrToInternal; /** @short Tree for the threading This tree is indexed by our internal ID. */ QHash threading; /** @short Last assigned internal ID */ uint threadingHelperLastId; /** @short Messages with unknown UIDs */ QSet unknownUids; /** @short Threading algorithm we're using for this request */ QByteArray requestedAlgorithm; /** @short Recursion guard for "is the model currently being reset?" We can't be sure what happens when we call rowCount() from updateNoThreading(). It is possible that the rowCount() would propagate to Model's askForMessagesInMailbox(), which could in turn call beginInsertRows, leading to a possible recursion. */ bool modelResetInProgress; QModelIndexList oldPersistentIndexes; QList oldPtrs; /** @short There's a pending THREAD command for which we haven't received data yet */ bool threadingInFlight; /** @short Is threading enabled, or shall we just use other features like sorting and filtering? */ bool m_shallBeThreading; /** @short Task handling the SORT command */ QPointer m_sortTask; /** @short Shall we sort in a reversed order? */ bool m_sortReverse; /** @short IDs of all thread roots when no sorting or filtering is applied */ QList threadedRootIds; /** @short Sorting criteria of the current copy of the sort result */ SortCriterium m_currentSortingCriteria; /** @short Search criteria of the current copy of the search/sort result */ QStringList m_currentSearchConditions; /** @short The current result of the SORT operation This variable holds the UIDs of all messages in this mailbox, sorted according to the current sorting criteria. */ Imap::Uids m_currentSortResult; /** @short Is the cached result of SEARCH/SORT fresh enough? */ typedef enum { RESULT_ASKED, /**< We've asked for the data */ RESULT_FRESH, /**< The response has just arrived and didn't get invalidated since then */ RESULT_INVALIDATED /**< A new message has arrived, rendering our copy invalid */ } ResultValidity; ResultValidity m_searchValidity; QTimer *m_delayedPrune; friend class ::ImapModelThreadingTest; // needs access to wantThreading(); }; } } #endif /* IMAP_THREADINGMSGLISTMODEL_H */