File indexing completed on 2024-11-24 04:53:12

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include <QDebug>
0024 #include <QFontMetrics>
0025 #include <QIcon>
0026 #include <QMimeData>
0027 #include "Imap/Model/DragAndDrop.h"
0028 #include "Imap/Model/MailboxTree.h"
0029 #include "Imap/Model/MailboxModel.h"
0030 #include "Imap/Model/MsgListModel.h"
0031 #include "UiUtils/IconLoader.h"
0032 
0033 
0034 namespace Imap
0035 {
0036 namespace Mailbox
0037 {
0038 
0039 MsgListModel::MsgListModel(QObject *parent, Model *model): QAbstractProxyModel(parent), msgListPtr(0), waitingForMessages(false)
0040 {
0041     setSourceModel(model);
0042 
0043     // FIXME: will need to be expanded when Model supports more signals...
0044     connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &MsgListModel::resetMe);
0045     connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged);
0046     connect(model, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged);
0047     connect(model, &QAbstractItemModel::dataChanged, this, &MsgListModel::handleDataChanged);
0048     connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MsgListModel::handleRowsAboutToBeRemoved);
0049     connect(model, &QAbstractItemModel::rowsRemoved, this, &MsgListModel::handleRowsRemoved);
0050     connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, &MsgListModel::handleRowsAboutToBeInserted);
0051     connect(model, &QAbstractItemModel::rowsInserted, this, &MsgListModel::handleRowsInserted);
0052 
0053     connect(this, &QAbstractItemModel::layoutChanged, this, &MsgListModel::indexStateChanged);
0054     connect(this, &QAbstractItemModel::modelReset, this, &MsgListModel::indexStateChanged);
0055     connect(this, &QAbstractItemModel::rowsInserted, this, &MsgListModel::indexStateChanged);
0056     connect(this, &QAbstractItemModel::rowsRemoved, this, &MsgListModel::indexStateChanged);
0057 }
0058 
0059 QHash<int, QByteArray> MsgListModel::roleNames() const
0060 {
0061     static QHash<int, QByteArray> roleNames;
0062     if (roleNames.isEmpty()) {
0063         roleNames[RoleIsFetched] = "isFetched";
0064         roleNames[RoleIsUnavailable] = "isUnavailable";
0065         roleNames[RoleMessageUid] = "messageUid";
0066         roleNames[RoleMessageIsMarkedDeleted] = "isMarkedDeleted";
0067         roleNames[RoleMessageIsMarkedRead] = "isMarkedRead";
0068         roleNames[RoleMessageIsMarkedForwarded] = "isMarkedForwarded";
0069         roleNames[RoleMessageIsMarkedReplied] = "isMarkedReplied";
0070         roleNames[RoleMessageIsMarkedRecent] = "isMarkedRecent";
0071         roleNames[RoleMessageIsMarkedFlagged] = "isMarkedFlagged";
0072         roleNames[RoleMessageIsMarkedJunk] = "isMarkedJunk";
0073         roleNames[RoleMessageIsMarkedNotJunk] = "isMarkedNotJunk";
0074         roleNames[RoleMessageDate] = "date";
0075         roleNames[RoleMessageInternalDate] = "receivedDate";
0076         roleNames[RoleMessageFrom] = "from";
0077         roleNames[RoleMessageTo] = "to";
0078         roleNames[RoleMessageCc] = "cc";
0079         roleNames[RoleMessageBcc] = "bcc";
0080         roleNames[RoleMessageSender] = "sender";
0081         roleNames[RoleMessageReplyTo] = "replyTo";
0082         roleNames[RoleMessageInReplyTo] = "inReplyTo";
0083         roleNames[RoleMessageMessageId] = "messageId";
0084         roleNames[RoleMessageSubject] = "subject";
0085         roleNames[RoleMessageFlags] = "flags";
0086         roleNames[RoleMessageSize] = "size";
0087         roleNames[RoleMessageFuzzyDate] = "fuzzyDate";
0088         roleNames[RoleMessageHasAttachments] = "hasAttachments";
0089     }
0090     return roleNames;
0091 }
0092 
0093 void MsgListModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0094 {
0095     QModelIndex first = mapFromSource(topLeft);
0096     QModelIndex second = mapFromSource(bottomRight);
0097 
0098     if (! first.isValid() || ! second.isValid()) {
0099         return;
0100     }
0101 
0102     second = createIndex(second.row(), COLUMN_COUNT - 1, Model::realTreeItem(second));
0103 
0104     if (first.parent() == second.parent()) {
0105         emit dataChanged(first, second);
0106     } else {
0107         Q_ASSERT(false);
0108         return;
0109     }
0110 }
0111 
0112 void MsgListModel::checkPersistentIndex() const
0113 {
0114     if (!msgList.isValid()) {
0115         if (msgListPtr) {
0116             emit const_cast<MsgListModel*>(this)->indexStateChanged();
0117         }
0118         msgListPtr = 0;
0119     }
0120 }
0121 
0122 QModelIndex MsgListModel::index(int row, int column, const QModelIndex &parent) const
0123 {
0124     checkPersistentIndex();
0125 
0126     if (! msgListPtr)
0127         return QModelIndex();
0128 
0129     if (parent.isValid())
0130         return QModelIndex();
0131 
0132     if (column < 0 || column >= COLUMN_COUNT)
0133         return QModelIndex();
0134 
0135     if (row >= msgListPtr->m_children.size() || row < 0)
0136         return QModelIndex();
0137 
0138     return createIndex(row, column, msgListPtr->m_children[row]);
0139 }
0140 
0141 QModelIndex MsgListModel::parent(const QModelIndex &index) const
0142 {
0143     Q_UNUSED(index);
0144     return QModelIndex();
0145 }
0146 
0147 bool MsgListModel::hasChildren(const QModelIndex &parent) const
0148 {
0149     return ! parent.isValid();
0150 }
0151 
0152 int MsgListModel::rowCount(const QModelIndex &parent) const
0153 {
0154     if (parent.isValid())
0155         return 0;
0156 
0157     checkPersistentIndex();
0158     if (! msgListPtr)
0159         return 0;
0160 
0161     return msgListPtr->rowCount(dynamic_cast<Model *>(sourceModel()));
0162 }
0163 
0164 int MsgListModel::columnCount(const QModelIndex &parent) const
0165 {
0166     if (parent.isValid())
0167         return 0;
0168     if (parent.column() != 0 && parent.column() != -1)
0169         return 0;
0170     return COLUMN_COUNT;
0171 }
0172 
0173 QModelIndex MsgListModel::mapToSource(const QModelIndex &proxyIndex) const
0174 {
0175     checkPersistentIndex();
0176     if (!msgListPtr || !proxyIndex.isValid())
0177         return QModelIndex();
0178 
0179     if (proxyIndex.parent().isValid())
0180         return QModelIndex();
0181 
0182     Model *model = dynamic_cast<Model *>(sourceModel());
0183     Q_ASSERT(model);
0184     return model->createIndex(proxyIndex.row(), 0, msgListPtr->m_children[proxyIndex.row()]);
0185 }
0186 
0187 QModelIndex MsgListModel::mapFromSource(const QModelIndex &sourceIndex) const
0188 {
0189     checkPersistentIndex();
0190     if (! msgListPtr)
0191         return QModelIndex();
0192 
0193     if (sourceIndex.model() != sourceModel())
0194         return QModelIndex();
0195     if (dynamic_cast<TreeItemMessage *>(Model::realTreeItem(sourceIndex))) {
0196         return index(sourceIndex.row(), 0, QModelIndex());
0197     } else {
0198         return QModelIndex();
0199     }
0200 }
0201 
0202 QVariant MsgListModel::data(const QModelIndex &proxyIndex, int role) const
0203 {
0204     checkPersistentIndex();
0205     if (! msgListPtr)
0206         return QVariant();
0207 
0208     if (! proxyIndex.isValid() || proxyIndex.model() != this)
0209         return QVariant();
0210 
0211     TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(Model::realTreeItem(proxyIndex));
0212     Q_ASSERT(message);
0213 
0214     switch (role) {
0215     case Qt::DisplayRole:
0216     case Qt::ToolTipRole:
0217         switch (proxyIndex.column()) {
0218         case SUBJECT:
0219             return QAbstractProxyModel::data(proxyIndex, Qt::DisplayRole);
0220         case FROM:
0221             return QLatin1String("[from]");
0222         case TO:
0223             return QLatin1String("[to]");
0224         case CC:
0225             return QLatin1String("[cc]");
0226         case BCC:
0227             return QLatin1String("[bcc]");
0228         case DATE:
0229             return message->envelope(static_cast<Model *>(sourceModel())).date;
0230         case RECEIVED_DATE:
0231             return message->internalDate(static_cast<Model *>(sourceModel()));
0232         case SIZE:
0233             return QVariant::fromValue(message->size(static_cast<Model *>(sourceModel())));
0234         default:
0235             return QVariant();
0236         }
0237 
0238     case RoleIsFetched:
0239     case RoleIsUnavailable:
0240     case RoleMessageUid:
0241     case RoleMessageIsMarkedDeleted:
0242     case RoleMessageIsMarkedRead:
0243     case RoleMessageIsMarkedForwarded:
0244     case RoleMessageIsMarkedReplied:
0245     case RoleMessageIsMarkedRecent:
0246     case RoleMessageIsMarkedFlagged:
0247     case RoleMessageIsMarkedJunk:
0248     case RoleMessageIsMarkedNotJunk:
0249     case RoleMessageDate:
0250     case RoleMessageFrom:
0251     case RoleMessageTo:
0252     case RoleMessageCc:
0253     case RoleMessageBcc:
0254     case RoleMessageSender:
0255     case RoleMessageReplyTo:
0256     case RoleMessageInReplyTo:
0257     case RoleMessageMessageId:
0258     case RoleMessageSubject:
0259     case RoleMessageFlags:
0260     case RoleMessageSize:
0261     case RoleMessageHeaderReferences:
0262     case RoleMessageHeaderListPost:
0263     case RoleMessageHeaderListPostNo:
0264     case RoleMessageHasAttachments:
0265         return dynamic_cast<TreeItemMessage *>(Model::realTreeItem(
0266                 proxyIndex))->data(static_cast<Model *>(sourceModel()), role);
0267     default:
0268         return QAbstractProxyModel::data(createIndex(proxyIndex.row(), 0, proxyIndex.internalPointer()), role);
0269     }
0270 }
0271 
0272 QVariant MsgListModel::headerData(int section, Qt::Orientation orientation, int role) const
0273 {
0274     if (orientation == Qt::Horizontal && role == Qt::DecorationRole && section == ATTACHMENT)
0275         return UiUtils::loadIcon(QStringLiteral("mail-attachment"));
0276 
0277     if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
0278         return QAbstractItemModel::headerData(section, orientation, role);
0279 
0280     switch (section) {
0281     case SUBJECT:
0282         return tr("Subject");
0283     case FROM:
0284         return tr("From");
0285     case TO:
0286         return tr("To");
0287     case CC:
0288         return tr("Cc");
0289     case BCC:
0290         return tr("Bcc");
0291     case DATE:
0292         return tr("Date");
0293     case RECEIVED_DATE:
0294         return tr("Received");
0295     case SIZE:
0296         return tr("Size");
0297     default:
0298         return QVariant();
0299     }
0300 }
0301 
0302 Qt::ItemFlags MsgListModel::flags(const QModelIndex &index) const
0303 {
0304     if (!index.isValid())
0305         return Qt::NoItemFlags;
0306 
0307     TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(Model::realTreeItem(index));
0308     Q_ASSERT(message);
0309 
0310     if (!message->uid())
0311         return Qt::NoItemFlags;
0312 
0313     return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled;
0314 }
0315 
0316 Qt::DropActions MsgListModel::supportedDragActions() const
0317 {
0318     return Qt::CopyAction | Qt::MoveAction;
0319 }
0320 
0321 Qt::DropActions MsgListModel::supportedDropActions() const
0322 {
0323     // Right now the GUI code doesn't allow dropping items into mailbox by dragging them
0324     // to a listing of mailbox' content, but if we just return Qt::IgnoreAction form here,
0325     // stuff breaks on Qt prior to 5.4.0 because on old Qt, the proxy models call
0326     // supportedDropActions() unless supportedDragActions is reimplemented on each level.
0327 #if QT_VERSION < QT_VERSION_CHECK(5, 4, 0)
0328     return Qt::CopyAction | Qt::MoveAction;
0329 #else
0330     return Qt::IgnoreAction;
0331 #endif
0332 }
0333 
0334 QStringList MsgListModel::mimeTypes() const
0335 {
0336     return QStringList() << MimeTypes::xTrojitaMessageList;
0337 }
0338 
0339 QMimeData *MsgListModel::mimeData(const QModelIndexList &indexes) const
0340 {
0341     if (indexes.isEmpty())
0342         return 0;
0343 
0344     QMimeData *res = new QMimeData();
0345     QByteArray encodedData;
0346     QDataStream stream(&encodedData, QIODevice::WriteOnly);
0347     stream.setVersion(QDataStream::Qt_4_6);
0348 
0349     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(Model::realTreeItem(
0350                                    indexes.front())->parent()->parent());
0351     Q_ASSERT(mailbox);
0352     stream << mailbox->mailbox() << mailbox->syncState.uidValidity();
0353 
0354     QList<uint> uids;
0355     for (QModelIndexList::const_iterator it = indexes.begin(); it != indexes.end(); ++it) {
0356         TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(Model::realTreeItem(*it));
0357         Q_ASSERT(message);
0358         Q_ASSERT(message->parent()->parent() == mailbox);
0359         Q_ASSERT(message->uid() > 0);
0360         uids << message->uid();
0361     }
0362     const QSet<uint> uidsSet(uids.begin(), uids.end());
0363     uids = uidsSet.values();
0364     stream << uids;
0365     res->setData(MimeTypes::xTrojitaMessageList, encodedData);
0366     return res;
0367 }
0368 
0369 void MsgListModel::resetMe()
0370 {
0371     beginResetModel();
0372     msgListPtr = 0;
0373     msgList = QModelIndex();
0374     endResetModel();
0375 }
0376 
0377 void MsgListModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
0378 {
0379     if (! parent.isValid()) {
0380         // Top-level items are tricky :(. As a quick hack, let's just die.
0381         resetMe();
0382         return;
0383     }
0384 
0385     checkPersistentIndex();
0386     if (! msgListPtr)
0387         return;
0388 
0389     TreeItem *item = Model::realTreeItem(parent);
0390     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(item);
0391     TreeItemMsgList *newList = dynamic_cast<TreeItemMsgList *>(item);
0392 
0393     if (parent.isValid()) {
0394         Q_ASSERT(parent.model() == sourceModel());
0395     } else {
0396         // a top-level mailbox might have been deleted, so we gotta setup proper pointer
0397         mailbox = static_cast<Model *>(sourceModel())->m_mailboxes;
0398         Q_ASSERT(mailbox);
0399     }
0400 
0401     if (newList) {
0402         if (newList == msgListPtr) {
0403             beginRemoveRows(mapFromSource(parent), start, end);
0404             for (int i = start; i <= end; ++i)
0405                 emit messageRemoved(msgListPtr->m_children[i]);
0406         }
0407     } else if (mailbox) {
0408         Q_ASSERT(start > 0);
0409         // if we're below it, we're gonna die
0410         for (int i = start; i <= end; ++i) {
0411             const Model *model = 0;
0412             QModelIndex translatedParent;
0413             Model::realTreeItem(parent, &model, &translatedParent);
0414             // FIXME: this assumes that no rows were removed by the proxy model
0415             TreeItemMailbox *m = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(model->index(i, 0, translatedParent).internalPointer()));
0416             Q_ASSERT(m);
0417             TreeItem *up = msgListPtr->parent();
0418             while (up) {
0419                 if (m == up) {
0420                     resetMe();
0421                     return;
0422                 }
0423                 up = up->parent();
0424             }
0425         }
0426     }
0427 }
0428 
0429 void MsgListModel::handleRowsRemoved(const QModelIndex &parent, int start, int end)
0430 {
0431     Q_UNUSED(start);
0432     Q_UNUSED(end);
0433 
0434     checkPersistentIndex();
0435     if (! msgListPtr)
0436         return;
0437 
0438     if (! parent.isValid()) {
0439         // already handled by resetMe() in handleRowsAboutToBeRemoved()
0440         return;
0441     }
0442 
0443     if (dynamic_cast<TreeItemMsgList *>(Model::realTreeItem(parent)) == msgListPtr)
0444         endRemoveRows();
0445 }
0446 
0447 void MsgListModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
0448 {
0449     checkPersistentIndex();
0450     if (! parent.isValid())
0451         return;
0452 
0453     TreeItemMsgList *newList = dynamic_cast<TreeItemMsgList *>(Model::realTreeItem(parent));
0454     if (msgListPtr && msgListPtr == newList) {
0455         beginInsertRows(mapFromSource(parent), start, end);
0456     }
0457 }
0458 
0459 void MsgListModel::handleRowsInserted(const QModelIndex &parent, int start, int end)
0460 {
0461     checkPersistentIndex();
0462     if (! parent.isValid())
0463         return;
0464 
0465     Q_UNUSED(start);
0466     Q_UNUSED(end);
0467     TreeItemMsgList *newList = dynamic_cast<TreeItemMsgList *>(Model::realTreeItem(parent));
0468     if (msgListPtr && msgListPtr == newList) {
0469         endInsertRows();
0470     }
0471 
0472     if (waitingForMessages) {
0473         waitingForMessages = false;
0474         emit messagesAvailable();
0475     }
0476 }
0477 
0478 void MsgListModel::setMailbox(const QModelIndex &index)
0479 {
0480     if (!index.isValid() || !index.data(Imap::Mailbox::RoleMailboxIsSelectable).toBool())
0481         return;
0482 
0483     waitingForMessages = true;
0484 
0485     const Model *model = 0;
0486     TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>(Model::realTreeItem(index, &model));
0487     Q_ASSERT(mbox);
0488     TreeItemMsgList *newList = dynamic_cast<TreeItemMsgList *>(
0489                                    mbox->child(0, const_cast<Model *>(model)));
0490     Q_ASSERT(newList);
0491     checkPersistentIndex();
0492     if (newList != msgListPtr) {
0493         msgListPtr = newList;
0494         msgList = msgListPtr->toIndex(const_cast<Model*>(model));
0495         msgListPtr->resetWasUnreadState();
0496         beginResetModel();
0497         endResetModel();
0498         emit mailboxChanged(index);
0499     }
0500 
0501     // We want to tell the Model that it should consider starting the IDLE command.
0502     // This shall happen regardless on what this model's idea about a "current mailbox" is because the IMAP connection might have
0503     // switched to another mailbox behind the scenes.
0504     const_cast<Model *>(model)->switchToMailbox(index);
0505 }
0506 
0507 /** @short Change mailbox to the one specified by its name */
0508 void MsgListModel::setMailbox(const QString &mailboxName)
0509 {
0510     Model *model = dynamic_cast<Model*>(sourceModel());
0511     Q_ASSERT(model);
0512     TreeItemMailbox *mailboxPtr = model->findMailboxByName(mailboxName);
0513     if (mailboxPtr)
0514         setMailbox(mailboxPtr->toIndex(model));
0515 }
0516 
0517 QModelIndex MsgListModel::currentMailbox() const
0518 {
0519     checkPersistentIndex();
0520     return msgList.parent();
0521 }
0522 
0523 bool MsgListModel::itemsValid() const
0524 {
0525     return currentMailbox().isValid();
0526 }
0527 
0528 }
0529 }