diff --git a/messagelist/src/core/item.cpp b/messagelist/src/core/item.cpp index 1b2135bb..77247c69 100644 --- a/messagelist/src/core/item.cpp +++ b/messagelist/src/core/item.cpp @@ -1,689 +1,699 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/item.h" #include "core/item_p.h" #include "core/model.h" #include "core/manager.h" #include // for KIO::filesize_t and related functions #include // kdepimlibs #include using namespace MessageList::Core; Item::Item(Type type) : d_ptr(new ItemPrivate(this)) { d_ptr->mType = type; } Item::Item(Item::Type type, ItemPrivate *dd) : d_ptr(dd) { d_ptr->mType = type; } Item::~Item() { killAllChildItems(); if (d_ptr->mParent) { d_ptr->mParent->d_ptr->childItemDead(this); } delete d_ptr; } void Item::childItemStats(ChildItemStats &stats) const { Q_ASSERT(d_ptr->mChildItems); stats.mTotalChildCount += d_ptr->mChildItems->count(); for (const auto child : qAsConst(*d_ptr->mChildItems)) { if (!child->status().isRead()) { stats.mUnreadChildCount++; } if (child->d_ptr->mChildItems) { child->childItemStats(stats); } } } QList< Item * > *Item::childItems() const { return d_ptr->mChildItems; } Item *Item::childItem(int idx) const { if (idx < 0) { return nullptr; } if (!d_ptr->mChildItems) { return nullptr; } if (d_ptr->mChildItems->count() <= idx) { return nullptr; } return d_ptr->mChildItems->at(idx); } Item *Item::firstChildItem() const { return d_ptr->mChildItems ? (d_ptr->mChildItems->count() > 0 ? d_ptr->mChildItems->at(0) : nullptr) : nullptr; } Item *Item::itemBelowChild(Item *child) { Q_ASSERT(d_ptr->mChildItems); int idx = indexOfChildItem(child); Q_ASSERT(idx >= 0); idx++; if (idx < d_ptr->mChildItems->count()) { return d_ptr->mChildItems->at(idx); } if (!d_ptr->mParent) { return nullptr; } return d_ptr->mParent->itemBelowChild(this); } Item *Item::itemBelow() { if (d_ptr->mChildItems) { if (!d_ptr->mChildItems->isEmpty()) { return d_ptr->mChildItems->at(0); } } if (!d_ptr->mParent) { return nullptr; } return d_ptr->mParent->itemBelowChild(this); } Item *Item::deepestItem() { if (d_ptr->mChildItems) { if (!d_ptr->mChildItems->isEmpty()) { return d_ptr->mChildItems->at(d_ptr->mChildItems->count() - 1)->deepestItem(); } } return this; } Item *Item::itemAboveChild(Item *child) { if (d_ptr->mChildItems) { int idx = indexOfChildItem(child); Q_ASSERT(idx >= 0); idx--; if (idx >= 0) { return d_ptr->mChildItems->at(idx); } } return this; } Item *Item::itemAbove() { if (!d_ptr->mParent) { return nullptr; } Item *siblingAbove = d_ptr->mParent->itemAboveChild(this); if (siblingAbove && siblingAbove != this && siblingAbove != d_ptr->mParent && siblingAbove->childItemCount() > 0) { return siblingAbove->deepestItem(); } return d_ptr->mParent->itemAboveChild(this); } int Item::childItemCount() const { return d_ptr->mChildItems ? d_ptr->mChildItems->count() : 0; } bool Item::hasChildren() const { return childItemCount() > 0; } int Item::indexOfChildItem(Item *child) const { if (!d_ptr->mChildItems) { return -1; } int idx = child->d_ptr->mThisItemIndexGuess; if (idx < d_ptr->mChildItems->count() && d_ptr->mChildItems->at(idx) == child) { return idx; // good guess } // We had a guess but it's out-of-date. Let's use the old guess as our // starting point and search in both directions from it. It's more likely we // will find the new position by going from the old guess rather than scanning // the list from the beginning. The worst case scenario is equal to not having // any guess at all. if (idx > 0 && idx < d_ptr->mChildItems->count()) { const auto begin = d_ptr->mChildItems->cbegin(); const auto end = d_ptr->mChildItems->cend(); auto fwdIt = begin + idx; auto bwdIt = fwdIt; idx = -1; // invalidate idx so it's -1 in case we fail to find the item while (fwdIt != end || bwdIt != end) { if (fwdIt != end) { if (++fwdIt != end && (*fwdIt) == child) { idx = std::distance(begin, fwdIt); break; } } if (bwdIt != end) { // sic! Q_ASSERT(bwdIt != begin); if ((*--bwdIt) == child) { idx = std::distance(begin, bwdIt); break; } if (bwdIt == begin) { // invalidate the iterator if we just checked the first item bwdIt = end; } } } } else { idx = d_ptr->mChildItems->indexOf(child); } if (idx >= 0) { Q_ASSERT(d_ptr->mChildItems->at(idx) == child); // make sure the above algorithm works child->d_ptr->mThisItemIndexGuess = idx; } return idx; } void Item::setIndexGuess(int index) { d_ptr->mThisItemIndexGuess = index; } Item *Item::topmostNonRoot() { Q_ASSERT(d_ptr->mType != InvisibleRoot); if (!d_ptr->mParent) { return this; } if (d_ptr->mParent->type() == InvisibleRoot) { return this; } return d_ptr->mParent->topmostNonRoot(); } static inline void append_string(QString &buffer, const QString &append) { if (!buffer.isEmpty()) { buffer += QLatin1String(", "); } buffer += append; } QString Item::statusDescription() const { QString ret; if (status().isRead()) { append_string(ret, i18nc("Status of an item", "Read")); } else { append_string(ret, i18nc("Status of an item", "Unread")); } if (status().hasAttachment()) { append_string(ret, i18nc("Status of an item", "Has Attachment")); } if (status().isToAct()) { append_string(ret, i18nc("Status of an item", "Action Item")); } if (status().isReplied()) { append_string(ret, i18nc("Status of an item", "Replied")); } if (status().isForwarded()) { append_string(ret, i18nc("Status of an item", "Forwarded")); } if (status().isSent()) { append_string(ret, i18nc("Status of an item", "Sent")); } if (status().isImportant()) { append_string(ret, i18nc("Status of an item", "Important")); } if (status().isSpam()) { append_string(ret, i18nc("Status of an item", "Spam")); } if (status().isHam()) { append_string(ret, i18nc("Status of an item", "Ham")); } if (status().isWatched()) { append_string(ret, i18nc("Status of an item", "Watched")); } if (status().isIgnored()) { append_string(ret, i18nc("Status of an item", "Ignored")); } return ret; } QString Item::formattedSize() const { return KIO::convertSize((KIO::filesize_t)size()); } QString Item::formattedDate() const { if (static_cast< uint >(date()) == static_cast< uint >(-1)) { return Manager::instance()->cachedLocalizedUnknownText(); } else { return Manager::instance()->dateFormatter()->dateString(date()); } } QString Item::formattedMaxDate() const { if (static_cast< uint >(maxDate()) == static_cast< uint >(-1)) { return Manager::instance()->cachedLocalizedUnknownText(); } else { return Manager::instance()->dateFormatter()->dateString(maxDate()); } } bool Item::recomputeMaxDate() { time_t newMaxDate = d_ptr->mDate; if (d_ptr->mChildItems) { for (auto child : qAsConst(*d_ptr->mChildItems)) { if (child->d_ptr->mMaxDate > newMaxDate) { newMaxDate = child->d_ptr->mMaxDate; } } } if (newMaxDate != d_ptr->mMaxDate) { setMaxDate(newMaxDate); return true; } return false; } Item::Type Item::type() const { return d_ptr->mType; } Item::InitialExpandStatus Item::initialExpandStatus() const { return d_ptr->mInitialExpandStatus; } void Item::setInitialExpandStatus(InitialExpandStatus initialExpandStatus) { d_ptr->mInitialExpandStatus = initialExpandStatus; } bool Item::isViewable() const { return d_ptr->mIsViewable; } bool Item::hasAncestor(const Item *it) const { return d_ptr->mParent ? (d_ptr->mParent == it ? true : d_ptr->mParent->hasAncestor(it)) : false; } void Item::setViewable(Model *model, bool bViewable) { if (d_ptr->mIsViewable == bViewable) { return; } if (!d_ptr->mChildItems) { d_ptr->mIsViewable = bViewable; return; } if (d_ptr->mChildItems->isEmpty()) { d_ptr->mIsViewable = bViewable; return; } if (bViewable) { if (model) { // fake having no children, for a second QList< Item * > *tmp = d_ptr->mChildItems; d_ptr->mChildItems = nullptr; //qDebug("BEGIN INSERT ROWS FOR PARENT %x: from %d to %d, (will) have %d children",this,0,tmp->count()-1,tmp->count()); model->beginInsertRows(model->index(this, 0), 0, tmp->count() - 1); d_ptr->mChildItems = tmp; d_ptr->mIsViewable = true; model->endInsertRows(); } else { d_ptr->mIsViewable = true; } for (const auto child : qAsConst(*d_ptr->mChildItems)) { child->setViewable(model, bViewable); } } else { for (const auto child : qAsConst(*d_ptr->mChildItems)) { child->setViewable(model, bViewable); } // It seems that we can avoid removing child items here since the parent has been removed: this is a hack tough // and should check if Qt4 still supports it in the next (hopefully largely fixed) release if (model) { // fake having no children, for a second model->beginRemoveRows(model->index(this, 0), 0, d_ptr->mChildItems->count() - 1); QList< Item * > *tmp = d_ptr->mChildItems; d_ptr->mChildItems = nullptr; d_ptr->mIsViewable = false; model->endRemoveRows(); d_ptr->mChildItems = tmp; } else { d_ptr->mIsViewable = false; } } } void Item::killAllChildItems() { if (!d_ptr->mChildItems) { return; } while (!d_ptr->mChildItems->isEmpty()) { delete d_ptr->mChildItems->first(); // this will call childDead() which will remove the child from the list } delete d_ptr->mChildItems; d_ptr->mChildItems = nullptr; } Item *Item::parent() const { return d_ptr->mParent; } void Item::setParent(Item *pParent) { d_ptr->mParent = pParent; } const Akonadi::MessageStatus &Item::status() const { return d_ptr->mStatus; } void Item::setStatus(Akonadi::MessageStatus status) { d_ptr->mStatus = status; } size_t Item::size() const { return d_ptr->mSize; } void Item::setSize(size_t size) { d_ptr->mSize = size; } time_t Item::date() const { return d_ptr->mDate; } void Item::setDate(time_t date) { d_ptr->mDate = date; } time_t Item::maxDate() const { return d_ptr->mMaxDate; } void Item::setMaxDate(time_t date) { d_ptr->mMaxDate = date; } const QString &Item::sender() const { return d_ptr->mSender; } void Item::setSender(const QString &sender) { d_ptr->mSender = sender; } QString Item::displaySender() const { return sender(); } const QString &Item::receiver() const { return d_ptr->mReceiver; } void Item::setReceiver(const QString &receiver) { d_ptr->mReceiver = receiver; } QString Item::displayReceiver() const { return receiver(); } const QString &Item::senderOrReceiver() const { return d_ptr->mUseReceiver ? d_ptr->mReceiver : d_ptr->mSender; } QString Item::displaySenderOrReceiver() const { return senderOrReceiver(); } bool Item::useReceiver() const { return d_ptr->mUseReceiver; } const QString &Item::subject() const { return d_ptr->mSubject; } void Item::setSubject(const QString &subject) { d_ptr->mSubject = subject; } +const QString &Item::folder() const +{ + return d_ptr->mFolder; +} + +void Item::setFolder(const QString &folder) +{ + d_ptr->mFolder = folder; +} + void MessageList::Core::Item::initialSetup(time_t date, size_t size, const QString &sender, const QString &receiver, bool useReceiver) { d_ptr->mDate = date; d_ptr->mMaxDate = date; d_ptr->mSize = size; d_ptr->mSender = sender; d_ptr->mReceiver = receiver; d_ptr->mUseReceiver = useReceiver; } void MessageList::Core::Item::setItemId(qint64 id) { d_ptr->mItemId = id; } qint64 MessageList::Core::Item::itemId() const { return d_ptr->mItemId; } void Item::setParentCollectionId(qint64 id) { d_ptr->mParentCollectionId = id; } qint64 Item::parentCollectionId() const { return d_ptr->mParentCollectionId; } void MessageList::Core::Item::setSubjectAndStatus(const QString &subject, Akonadi::MessageStatus status) { d_ptr->mSubject = subject; d_ptr->mStatus = status; } // FIXME: Try to "cache item insertions" and call beginInsertRows() and endInsertRows() in a chunked fashion... void Item::rawAppendChildItem(Item *child) { if (!d_ptr->mChildItems) { d_ptr->mChildItems = new QList< Item * >(); } d_ptr->mChildItems->append(child); } int Item::appendChildItem(Model *model, Item *child) { if (!d_ptr->mChildItems) { d_ptr->mChildItems = new QList< Item * >(); } const int idx = d_ptr->mChildItems->count(); if (d_ptr->mIsViewable) { if (model) { model->beginInsertRows(model->index(this, 0), idx, idx); // THIS IS EXTREMELY UGLY, BUT IT'S THE ONLY POSSIBLE WAY WITH QT4 AT THE TIME OF WRITING } d_ptr->mChildItems->append(child); child->setIndexGuess(idx); if (model) { model->endInsertRows(); // THIS IS EXTREMELY UGLY, BUT IT'S THE ONLY POSSIBLE WAY WITH QT4 AT THE TIME OF WRITING } child->setViewable(model, true); } else { d_ptr->mChildItems->append(child); child->setIndexGuess(idx); } return idx; } void Item::dump(const QString &prefix) { QString out = QStringLiteral("%1 %x VIEWABLE:%2").arg(prefix, d_ptr->mIsViewable ? QStringLiteral("yes") : QStringLiteral("no")); qDebug(out.toUtf8().data(), this); QString nPrefix(prefix); nPrefix += QLatin1String(" "); if (!d_ptr->mChildItems) { return; } for (const auto child : qAsConst(*d_ptr->mChildItems)) { child->dump(nPrefix); } } void Item::takeChildItem(Model *model, Item *child) { if (!d_ptr->mChildItems) { return; // Ugh... not our child ? } if (!d_ptr->mIsViewable) { //qDebug("TAKING NON VIEWABLE CHILD ITEM %x",child); // We can highly optimize this case d_ptr->mChildItems->removeOne(child); #if 0 // This *could* be done, but we optimize and avoid it. if (d->mChildItems->isEmpty()) { delete d->mChildItems; d->mChildItems = 0; } #endif child->setParent(nullptr); return; } const int idx = indexOfChildItem(child); if (idx < 0) { return; // Aaargh... not our child ? } child->setViewable(model, false); if (model) { model->beginRemoveRows(model->index(this, 0), idx, idx); } child->setParent(nullptr); d_ptr->mChildItems->removeAt(idx); if (model) { model->endRemoveRows(); } #if 0 // This *could* be done, but we optimize and avoid it. if (d->mChildItems->isEmpty()) { delete d->mChildItems; d->mChildItems = 0; } #endif } void ItemPrivate::childItemDead(Item *child) { // mChildItems MUST be non zero here, if it's not then it's a bug in THIS FILE mChildItems->removeOne(child); // since we always have ONE (if we not, it's a bug) } diff --git a/messagelist/src/core/item.h b/messagelist/src/core/item.h index 60f57a38..8f050aca 100644 --- a/messagelist/src/core/item.h +++ b/messagelist/src/core/item.h @@ -1,413 +1,423 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef MESSAGELIST_CORE_ITEM_H #define MESSAGELIST_CORE_ITEM_H #include #include #include // for time_t #include #include #include #include namespace MessageList { namespace Core { class ItemPrivate; /** * A single item of the MessageList tree managed by MessageList::Model. * * This class stores basic information needed in all the subclasses which * at the moment of writing are MessageItem and GroupHeaderItem. */ class MESSAGELIST_EXPORT Item { friend class Model; friend class ModelPrivate; public: /** * The type of the Item. */ enum Type { GroupHeader, ///< This item is a GroupHeaderItem Message, ///< This item is a MessageItem InvisibleRoot ///< This item is just Item and it's the only InvisibleRoot per Model. }; /** * Specifies the initial expand status for the item that should be applied * when it's attached to the viewable tree. Needed as a workaround for * QTreeView limitations in handling item expansion. */ enum InitialExpandStatus { ExpandNeeded, ///< Must expand when this item becomes viewable NoExpandNeeded, ///< No expand needed at all ExpandExecuted ///< Item already expanded }; protected: /** * Creates an Item. Only derived classes and MessageList::Model should access this. */ Item(Type type); Item(Type type, ItemPrivate *dd); public: /** * Destroys the Item. Should be protected just like the constructor but the QList<> * helpers need to access it, so it's public actually. */ virtual ~Item(); /** * Returns the type of this item. The Type can be set only in the constructor. */ Q_REQUIRED_RESULT Type type() const; /** * The initial expand status we have to honor when attaching to the viewable root. */ Q_REQUIRED_RESULT InitialExpandStatus initialExpandStatus() const; /** * Set the initial expand status we have to honor when attaching to the viewable root. */ void setInitialExpandStatus(InitialExpandStatus initialExpandStatus); /** * Is this item attached to the viewable root ? */ Q_REQUIRED_RESULT bool isViewable() const; /** * Return true if Item pointed by it is an ancestor of this item (that is, * if it is its parent, parent of its parent, parent of its parent of its parent etc... */ Q_REQUIRED_RESULT bool hasAncestor(const Item *it) const; /** * Makes this item viewable, that is, notifies its existence to any listener * attached to the "rowsInserted()" signal, most notably QTreeView. * * This will also make all the children viewable. */ void setViewable(Model *model, bool bViewable); /** * Return the list of child items. May be null. */ Q_REQUIRED_RESULT QList< Item * > *childItems() const; /** * Returns the child item at position idx or 0 if idx is out of the allowable range. */ Q_REQUIRED_RESULT Item *childItem(int idx) const; /** * Returns the first child item, if any. */ Q_REQUIRED_RESULT Item *firstChildItem() const; /** * Returns the item that is visually below the specified child if this item. * Note that the returned item may belong to a completely different subtree. */ Q_REQUIRED_RESULT Item *itemBelowChild(Item *child); /** * Returns the item that is visually above the specified child if this item. * Note that the returned item may belong to a completely different subtree. */ Q_REQUIRED_RESULT Item *itemAboveChild(Item *child); /** * Returns the deepest item in the subtree originating at this item. */ Q_REQUIRED_RESULT Item *deepestItem(); /** * Returns the item that is visually below this item in the tree. * Note that the returned item may belong to a completely different subtree. */ Q_REQUIRED_RESULT Item *itemBelow(); /** * Returns the item that is visually above this item in the tree. * Note that the returned item may belong to a completely different subtree. */ Q_REQUIRED_RESULT Item *itemAbove(); /** * Debug helper. Dumps the structure of this subtree. */ void dump(const QString &prefix); /** * Returns the number of children of this Item. */ Q_REQUIRED_RESULT int childItemCount() const; /** * Convenience function that returns true if this item has children. */ Q_REQUIRED_RESULT bool hasChildren() const; /** * A structure used with MessageList::Item::childItemStats(). * Contains counts of total and unread messages in a subtree. */ class ChildItemStats { public: unsigned int mTotalChildCount; // total unsigned int mUnreadChildCount; // unread only public: ChildItemStats() : mTotalChildCount(0) , mUnreadChildCount(0) { } }; /** * Gathers statistics about child items. * For performance purposes assumes that this item has children. * You MUST check it before calling it. */ void childItemStats(ChildItemStats &stats) const; /** * Returns the actual index of the child Item item or -1 if * item is not a child of this Item. */ int indexOfChildItem(Item *item) const; /** * Sets the cached guess for the index of this item in the parent's child list. * * This is used to speed up the index lookup with the following algorithm: * Ask the parent if this item is at the position specified by index guess (this costs ~O(1)). * If the position matches we have finished, if it doesn't then perform * a linear search via indexOfChildItem() (which costs ~O(n)). */ void setIndexGuess(int index); /** * Returns the parent Item in the tree, or 0 if this item isn't attached to the tree. * Please note that even if this item has a non-zero parent, it can be still non viewable. * That is: the topmost parent of this item may be not attached to the viewable root. */ Q_REQUIRED_RESULT Item *parent() const; /** * Sets the parent for this item. You should also take care of inserting * this item in the parent's child list. */ void setParent(Item *pParent); /** * Returns the topmost parent item that is not a Root item (that is, is a Message or GroupHeader). */ Q_REQUIRED_RESULT Item *topmostNonRoot(); /** * Returns the status associated to this Item. */ const Akonadi::MessageStatus &status() const; /** * Sets the status associated to this Item. */ void setStatus(Akonadi::MessageStatus status); /** * Returns a string describing the status e.g: "Read, Forwarded, Important" */ Q_REQUIRED_RESULT QString statusDescription() const; /** * Returns the size of this item (size of the Message, mainly) */ size_t size() const; /** * Sets the size of this item (size of the Message, mainly) */ void setSize(size_t size); /** * A string with a text rappresentation of size(). This is computed on-the-fly * and not cached. */ Q_REQUIRED_RESULT QString formattedSize() const; /** * Returns the date of this item */ time_t date() const; /** * Sets the date of this item */ void setDate(time_t date); /** * A string with a text rappresentation of date() obtained via Manager. This is computed on-the-fly * and not cached. */ Q_REQUIRED_RESULT QString formattedDate() const; /** * Returns the maximum date in the subtree originating from this item. * This is kept up-to-date by MessageList::Model. */ time_t maxDate() const; /** * Sets the maximum date in the subtree originating from this item. */ void setMaxDate(time_t date); /** * A string with a text rappresentation of maxDate() obtained via Manager. This is computed on-the-fly * and not cached. */ Q_REQUIRED_RESULT QString formattedMaxDate() const; /** * Recompute the maximum date from the current children list. * Return true if the current max date changed and false otherwise. */ bool recomputeMaxDate(); /** * Returns the sender associated to this item. */ Q_REQUIRED_RESULT const QString &sender() const; /** * Sets the sender associated to this item. */ void setSender(const QString &sender); /** * Display sender. */ Q_REQUIRED_RESULT QString displaySender() const; /** * Returns the receiver associated to this item. */ const QString &receiver() const; /** * Sets the sender associated to this item. */ void setReceiver(const QString &receiver); /** * Display receiver. */ Q_REQUIRED_RESULT QString displayReceiver() const; /** * Returns the sender or the receiver, depending on the underlying StorageModel settings. */ const QString &senderOrReceiver() const; /** * Display sender or receiver. */ Q_REQUIRED_RESULT QString displaySenderOrReceiver() const; /** * Returns whether sender or receiver is supposed to be displayed. */ bool useReceiver() const; /** * Returns the subject associated to this Item. */ const QString &subject() const; /** * Sets the subject associated to this Item. */ void setSubject(const QString &subject); + + /** + * Returns the folder associated to this Item. + */ + const QString &folder() const; + + /** + * Sets the folder associated to this Item. + */ + void setFolder(const QString &folder); /** * This is meant to be called right after the constructor. * It sets up several items at once (so even if not inlined it's still a single call) * and it skips some calls that can be avoided at constructor time. */ void initialSetup(time_t date, size_t size, const QString &sender, const QString &receiver, bool useReceiver); void setItemId(qint64 id); Q_REQUIRED_RESULT qint64 itemId() const; void setParentCollectionId(qint64 id); Q_REQUIRED_RESULT qint64 parentCollectionId() const; /** * This is meant to be called right after the constructor for MessageItem objects. * It sets up several items at once (so even if not inlined it's still a single call). */ void setSubjectAndStatus(const QString &subject, Akonadi::MessageStatus status); /** * Appends an Item to this item's child list. * The Model is used for beginInsertRows()/endInsertRows() calls. */ int appendChildItem(Model *model, Item *child); /** * Appends a child item without inserting it via the model. * This is useful in ThemeEditor which doesn't use a custom model for the items. * You shouldn't need to use this function... */ void rawAppendChildItem(Item *child); /** * Removes a child from this item's child list without deleting it. * The Model is used for beginRemoveRows()/endRemoveRows() calls. */ void takeChildItem(Model *model, Item *child); /** * Kills all the child items without emitting any signal, recursively. * It should be used only when MessageList::Model is reset() afterwards. */ void killAllChildItems(); protected: ItemPrivate *const d_ptr; Q_DECLARE_PRIVATE(Item) }; } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_ITEM_H diff --git a/messagelist/src/core/item_p.h b/messagelist/src/core/item_p.h index cf4f1d88..2df246ac 100644 --- a/messagelist/src/core/item_p.h +++ b/messagelist/src/core/item_p.h @@ -1,509 +1,510 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef MESSAGELIST_CORE_ITEM_P_H #define MESSAGELIST_CORE_ITEM_P_H #include "core/item.h" #include "MessageCore/StringUtil" // See the MessageList::ItemPrivate::insertChildItem() function below for an explanation of this macro. #if __GNUC__ >= 3 //krazy:exclude=cpp #define GCC_DONT_INLINE_THIS __attribute__((noinline)) #else #define GCC_DONT_INLINE_THIS #endif namespace MessageList { namespace Core { class ItemPrivate { public: explicit ItemPrivate(Item *owner) : q(owner) , mChildItems(nullptr) , mParent(nullptr) , mThisItemIndexGuess(0) , mInitialExpandStatus(Item::NoExpandNeeded) , mIsViewable(false) , mUseReceiver(false) { } virtual ~ItemPrivate() { } /** * Implements "in the middle" insertions of child items. * The template argument class must export a static inline bool firstGreaterOrEqual( Item *, Item * ) * function which must return true when the first parameter item is considered to be greater * or equal to the second parameter item and false otherwise. * * The insertion function *IS* our very bottleneck on flat views * (when there are items with a lot of children). This is somewhat pathological... * beside the binary search based insertion sort we actually can only do "statement level" optimization. * I've found no better algorithms so far. If someone has a clever idea, please write to pragma * at kvirc dot net :) * * GCC_DONT_INLINE_THIS is a macro defined above to __attribute__((noinline)) * if the current compiler is gcc. Without this attribute gcc attempts to inline THIS * function inside the callers. The problem is that while inlining this function * it doesn't inline the INNER comparison functions (which we _WANT_ to be inlined) * because they would make the caller function too big. * * This is what gcc reports with -Winline: * * /home/pragma/kmail-soc/kmail/messagelistview/item.h:352: warning: inlining failed in call to * 'static bool MessageList::ItemSubjectComparator::firstGreaterOrEqual(MessageList::Item*, MessageList::Item*)': * --param large-function-growth limit reached while inlining the caller * /home/pragma/kmail-soc/kmail/messagelistview/model.cpp:239: warning: called from here * * The comparison functions then appear in the symbol table: * * etherea kmail # nm /usr/kde/4.0/lib/libkmailprivate.so | grep Comparator * 00000000005d2c10 W _ZN5KMail15MessageList18ItemDateComparator19firstGreaterOrEqualEPNS0_4ItemES3_ * 00000000005d2cb0 W _ZN5KMail15MessageList20ItemSenderComparator19firstGreaterOrEqualEPNS0_4ItemES3_ * 00000000005d2c50 W _ZN5KMail15MessageList21ItemSubjectComparator19firstGreaterOrEqualEPNS0_4ItemES3_ * ... * * With this attribute, instead, gcc doesn't complain at all and the inner comparisons * *seem* to be inlined correctly (there is no sign of them in the symbol table). */ template< class ItemComparator, bool bAscending > int GCC_DONT_INLINE_THIS insertChildItem(Model *model, Item *child) { if (!mChildItems) { return q->appendChildItem(model, child); } int cnt = mChildItems->count(); if (cnt < 1) { return q->appendChildItem(model, child); } int idx; Item *pivot; if (bAscending) { pivot = mChildItems->at(cnt - 1); if (ItemComparator::firstGreaterOrEqual(child, pivot)) { // gcc: <-- inline this instead, thnx return q->appendChildItem(model, child); // this is very likely in date based comparisons (FIXME: not in other ones) } // Binary search based insertion int l = 0; int h = cnt - 1; for (;;) { idx = (l + h) / 2; pivot = mChildItems->at(idx); if (ItemComparator::firstGreaterOrEqual(pivot, child)) { // gcc: <-- inline this instead, thnx if (l < h) { h = idx - 1; } else { break; } } else { if (l < h) { l = idx + 1; } else { idx++; break; } } } } else { pivot = mChildItems->at(0); if (ItemComparator::firstGreaterOrEqual(child, pivot)) { // gcc: <-- inline this instead, thnx idx = 0; // this is very likely in date based comparisons (FIXME: not in other ones) } else { // Binary search based insertion int l = 0; int h = cnt - 1; for (;;) { idx = (l + h) / 2; pivot = mChildItems->at(idx); if (ItemComparator::firstGreaterOrEqual(child, pivot)) { // gcc: <-- inline this instead, thnx if (l < h) { h = idx - 1; } else { break; } } else { if (l < h) { l = idx + 1; } else { idx++; break; } } } } } Q_ASSERT(idx >= 0); Q_ASSERT(idx <= mChildItems->count()); if (mIsViewable && model) { model->beginInsertRows(model->index(q, 0), idx, idx); // BLEAH :D } mChildItems->insert(idx, child); child->setIndexGuess(idx); if (mIsViewable) { if (model) { model->endInsertRows(); // BLEAH :D } child->setViewable(model, true); } return idx; } /** * Checks if the specified child item is actually in the wrong * position in the child list and returns true in that case. * Returns false if the item is already in the right position * and no re-sorting is needed. */ template< class ItemComparator, bool bAscending > bool childItemNeedsReSorting(Item *child) { if (!mChildItems) { return false; // not my child! (ugh... should assert ?) } const int idx = q->indexOfChildItem(child); if (idx > 0) { Item *prev = mChildItems->at(idx - 1); if (bAscending) { // child must be greater or equal to the previous item if (!ItemComparator::firstGreaterOrEqual(child, prev)) { return true; // wrong order: needs re-sorting } } else { // previous must be greater or equal to the child item if (!ItemComparator::firstGreaterOrEqual(prev, child)) { return true; // wrong order: needs re-sorting } } } if (idx < (mChildItems->count() - 1)) { Item *next = mChildItems->at(idx + 1); if (bAscending) { // next must be greater or equal to child if (!ItemComparator::firstGreaterOrEqual(next, child)) { return true; // wrong order: needs re-sorting } } else { // child must be greater or equal to next if (!ItemComparator::firstGreaterOrEqual(child, next)) { return true; // wrong order: needs re-sorting } } } return false; } /** * Internal handler for managing the children list. */ void childItemDead(Item *child); Item *const q; QList< Item * > *mChildItems; ///< List of children, may be 0 Item *mParent = nullptr; ///< The parent view item time_t mMaxDate; ///< The maximum date in the subtree time_t mDate; ///< The date of the message (or group date) size_t mSize; ///< The size of the message in bytes QString mSender; ///< The sender of the message (or group sender) QString mReceiver; ///< The receiver of the message (or group receiver) QString mSubject; ///< The subject of the message (or group subject) + QString mFolder; ///< The folder of the message qint64 mItemId; ///< The Akonadi item id qint64 mParentCollectionId; ///< The Akonadi ID of collection that this particular item comes from (can be virtual collection) Akonadi::MessageStatus mStatus; ///< The status of the message (may be extended to groups in the future) int mThisItemIndexGuess; ///< The guess for the index in the parent's child list Item::Type mType : 4; ///< The type of this item Item::InitialExpandStatus mInitialExpandStatus : 4; ///< The expand status we have to honor when we attach to the viewable root bool mIsViewable : 1; ///< Is this item attached to the viewable root ? bool mUseReceiver : 1; ///< senderOrReceiver() returns receiver rather than sender }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemSizeComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (first->size() < second->size()) { return false; } // When the sizes are equal compare by date too if (first->size() == second->size()) { return first->date() >= second->date(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemDateComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { // When the dates are equal compare by subject too // This is useful, for example, in kernel mailing list where people // send out multiple messages with patch parts at exactly the same time. if (first->date() == second->date()) { return first->subject() >= second->subject(); } if (first->date() == static_cast(-1)) { // invalid is always smaller return false; } if (second->date() == static_cast(-1)) { return true; } if (first->date() < second->date()) { return false; } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemMaxDateComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (first->maxDate() < second->maxDate()) { return false; } if (first->maxDate() == second->maxDate()) { return first->subject() >= second->subject(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemSubjectComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { const int ret = MessageCore::StringUtil::stripOffPrefixes(first->subject()). compare(MessageCore::StringUtil::stripOffPrefixes(second->subject()), Qt::CaseInsensitive); if (ret < 0) { return false; } // compare by date when subjects are equal if (ret == 0) { return first->date() >= second->date(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemSenderComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { const int ret = first->displaySender().compare( second->displaySender(), Qt::CaseInsensitive); if (ret < 0) { return false; } // compare by date when senders are equal if (ret == 0) { return first->date() >= second->date(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemReceiverComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { const int ret = first->displayReceiver().compare( second->displayReceiver(), Qt::CaseInsensitive); if (ret < 0) { return false; } // compare by date when receivers are equal if (ret == 0) { return first->date() >= second->date(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemSenderOrReceiverComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { const int ret = first->displaySenderOrReceiver().compare( second->displaySenderOrReceiver(), Qt::CaseInsensitive); if (ret < 0) { return false; } // compare by date when sender/receiver are equal if (ret == 0) { return first->date() >= second->date(); } return true; } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemActionItemStatusComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (first->status().isToAct()) { if (second->status().isToAct()) { return first->date() >= second->date(); } return true; } if (second->status().isToAct()) { return false; } return first->date() >= second->date(); } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemUnreadStatusComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (!first->status().isRead()) { // fist is unread if (!second->status().isRead()) { return first->date() >= second->date(); // both are unread } // unread comes always first with respect to non-unread return true; } if (!second->status().isRead()) { return false; } // both are read return first->date() >= second->date(); } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemImportantStatusComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (!first->status().isImportant()) { // fist is unread if (!second->status().isImportant()) { return first->date() >= second->date(); // both are unread } // unread comes always first with respect to non-unread return true; } if (!second->status().isImportant()) { return false; } // both are read return first->date() >= second->date(); } }; /** * A helper class used with MessageList::Item::childItemNeedsReSorting() and * MessageList::Item::insertChildItem(). */ class ItemAttachmentStatusComparator { public: static inline bool firstGreaterOrEqual(Item *first, Item *second) { if (!first->status().hasAttachment()) { // fist is unread if (!second->status().hasAttachment()) { return first->date() >= second->date(); // both are unread } // unread comes always first with respect to non-unread return true; } if (!second->status().hasAttachment()) { return false; } // both are read return first->date() >= second->date(); } }; } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_ITEM_P_H diff --git a/messagelist/src/core/theme.cpp b/messagelist/src/core/theme.cpp index 9bbc0114..84926987 100644 --- a/messagelist/src/core/theme.cpp +++ b/messagelist/src/core/theme.cpp @@ -1,1288 +1,1291 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/theme.h" #include #include #include #include #include #include #include "messagelist_debug.h" using namespace MessageList::Core; // // Theme versioning // // The themes simply have a DWORD version number attached. // The earliest version we're able to load is 0x1013. // // Theme revision history: // // Version Date introduced Description // -------------------------------------------------------------------------------------------------------------- // 0x1013 08.11.2008 Initial theme version, introduced when this piece of code has been moved into trunk. // 0x1014 12.11.2008 Added runtime column data: width and column visibility // 0x1015 03.03.2009 Added icon size // 0x1016 08.03.2009 Added support for sorting by New/Unread status // 0x1017 16.08.2009 Added support for column icon // 0x1018 17.01.2010 Added support for annotation icon // 0x1019 13.07.2010 Added support for invitation icon // static const int gThemeCurrentVersion = 0x1019; // increase if you add new fields or change the meaning of some // you don't need to change the values below, but you might want to add new ones static const int gThemeMinimumSupportedVersion = 0x1013; static const int gThemeMinimumVersionWithColumnRuntimeData = 0x1014; static const int gThemeMinimumVersionWithIconSizeField = 0x1015; static const int gThemeMinimumVersionWithSortingByUnreadStatusAllowed = 0x1016; static const int gThemeMinimumVersionWithColumnIcon = 0x1017; static const int gThemeMinimumVersionWithAnnotationIcon = 0x1018; static const int gThemeMinimumVersionWithInvitationIcon = 0x1019; // the default icon size static const int gThemeDefaultIconSize = 16; Theme::ContentItem::ContentItem(Type type) : mType(type) , mFlags(0) { } Theme::ContentItem::ContentItem(const ContentItem &src) : mType(src.mType) , mFlags(src.mFlags) , mCustomColor(src.mCustomColor) { } Theme::ContentItem::Type Theme::ContentItem::type() const { return mType; } bool Theme::ContentItem::canBeDisabled() const { return (static_cast< int >(mType) & CanBeDisabled) != 0; } bool Theme::ContentItem::canUseCustomColor() const { return (static_cast< int >(mType) & CanUseCustomColor) != 0; } bool Theme::ContentItem::displaysText() const { return (static_cast< int >(mType) & DisplaysText) != 0; } bool Theme::ContentItem::displaysLongText() const { return (static_cast< int >(mType) & LongText) != 0; } bool Theme::ContentItem::isIcon() const { return (static_cast< int >(mType) & IsIcon) != 0; } bool Theme::ContentItem::isClickable() const { return (static_cast< int >(mType) & IsClickable) != 0; } bool Theme::ContentItem::isSpacer() const { return (static_cast< int >(mType) & IsSpacer) != 0; } QString Theme::ContentItem::description(Type type) { switch (type) { case Subject: return i18nc("Description of Type Subject", "Subject"); break; case Date: return i18nc("Description of Type Date", "Date"); break; case SenderOrReceiver: return i18n("Sender/Receiver"); break; case Sender: return i18nc("Description of Type Sender", "Sender"); break; case Receiver: return i18nc("Description of Type Receiver", "Receiver"); break; case Size: return i18nc("Description of Type Size", "Size"); break; case ReadStateIcon: return i18n("Unread/Read Icon"); break; case AttachmentStateIcon: return i18n("Attachment Icon"); break; case RepliedStateIcon: return i18n("Replied/Forwarded Icon"); break; case CombinedReadRepliedStateIcon: return i18n("Combined New/Unread/Read/Replied/Forwarded Icon"); break; case ActionItemStateIcon: return i18n("Action Item Icon"); break; case ImportantStateIcon: return i18n("Important Icon"); break; case GroupHeaderLabel: return i18n("Group Header Label"); break; case SpamHamStateIcon: return i18n("Spam/Ham Icon"); break; case WatchedIgnoredStateIcon: return i18n("Watched/Ignored Icon"); break; case ExpandedStateIcon: return i18n("Group Header Expand/Collapse Icon"); break; case EncryptionStateIcon: return i18n("Encryption State Icon"); break; case SignatureStateIcon: return i18n("Signature State Icon"); break; case VerticalLine: return i18n("Vertical Separation Line"); break; case HorizontalSpacer: return i18n("Horizontal Spacer"); break; case MostRecentDate: return i18n("Max Date"); break; case TagList: return i18n("Message Tags"); break; case AnnotationIcon: return i18n("Note Icon"); case InvitationIcon: return i18n("Invitation Icon"); + case Folder: + return i18nc("Description of Type Folder", "Folder"); default: return i18nc("Description for an Unknown Type", "Unknown"); break; } } bool Theme::ContentItem::useCustomColor() const { return mFlags & UseCustomColor; } void Theme::ContentItem::setUseCustomColor(bool useCustomColor) { if (useCustomColor) { mFlags |= UseCustomColor; } else { mFlags &= ~UseCustomColor; } } bool Theme::ContentItem::isBold() const { return mFlags & IsBold; } void Theme::ContentItem::setBold(bool isBold) { if (isBold) { mFlags |= IsBold; } else { mFlags &= ~IsBold; } } bool Theme::ContentItem::isItalic() const { return mFlags & IsItalic; } void Theme::ContentItem::setItalic(bool isItalic) { if (isItalic) { mFlags |= IsItalic; } else { mFlags &= ~IsItalic; } } bool Theme::ContentItem::hideWhenDisabled() const { return mFlags & HideWhenDisabled; } void Theme::ContentItem::setHideWhenDisabled(bool hideWhenDisabled) { if (hideWhenDisabled) { mFlags |= HideWhenDisabled; } else { mFlags &= ~HideWhenDisabled; } } bool Theme::ContentItem::softenByBlendingWhenDisabled() const { return mFlags & SoftenByBlendingWhenDisabled; } void Theme::ContentItem::setSoftenByBlendingWhenDisabled(bool softenByBlendingWhenDisabled) { if (softenByBlendingWhenDisabled) { mFlags |= SoftenByBlendingWhenDisabled; } else { mFlags &= ~SoftenByBlendingWhenDisabled; } } bool Theme::ContentItem::softenByBlending() const { return mFlags & SoftenByBlending; } void Theme::ContentItem::setSoftenByBlending(bool softenByBlending) { if (softenByBlending) { mFlags |= SoftenByBlending; } else { mFlags &= ~SoftenByBlending; } } const QColor &Theme::ContentItem::customColor() const { return mCustomColor; } void Theme::ContentItem::setCustomColor(const QColor &clr) { mCustomColor = clr; } bool Theme::ContentItem::applicableToMessageItems(Type type) { return static_cast< int >(type) & ApplicableToMessageItems; } bool Theme::ContentItem::applicableToGroupHeaderItems(Type type) { return static_cast< int >(type) & ApplicableToGroupHeaderItems; } void Theme::ContentItem::save(QDataStream &stream) const { stream << (int)mType; stream << mFlags; stream << mCustomColor; } bool Theme::ContentItem::load(QDataStream &stream, int /*themeVersion*/) { int val; stream >> val; mType = static_cast< Type >(val); switch (mType) { case Subject: case Date: case SenderOrReceiver: case Sender: case Receiver: case Size: case ReadStateIcon: case AttachmentStateIcon: case RepliedStateIcon: case GroupHeaderLabel: case ActionItemStateIcon: case ImportantStateIcon: case SpamHamStateIcon: case WatchedIgnoredStateIcon: case ExpandedStateIcon: case EncryptionStateIcon: case SignatureStateIcon: case VerticalLine: case HorizontalSpacer: case MostRecentDate: case CombinedReadRepliedStateIcon: case TagList: case AnnotationIcon: case InvitationIcon: + case Folder: // ok break; default: qCDebug(MESSAGELIST_LOG) << "Invalid content item type"; return false; // b0rken break; } stream >> mFlags; stream >> mCustomColor; if (mFlags & UseCustomColor) { if (!mCustomColor.isValid()) { mFlags &= ~UseCustomColor; } } return true; } Theme::Row::Row() { } Theme::Row::Row(const Row &src) { for (const auto ci : qAsConst(src.mLeftItems)) { addLeftItem(new ContentItem(*ci)); } for (const auto ci : qAsConst(src.mRightItems)) { addRightItem(new ContentItem(*ci)); } } Theme::Row::~Row() { removeAllLeftItems(); removeAllRightItems(); } void Theme::Row::removeAllLeftItems() { while (!mLeftItems.isEmpty()) { delete mLeftItems.takeFirst(); } } void Theme::Row::addLeftItem(Theme::ContentItem *item) { mLeftItems.append(item); } void Theme::Row::removeAllRightItems() { while (!mRightItems.isEmpty()) { delete mRightItems.takeFirst(); } } void Theme::Row::addRightItem(Theme::ContentItem *item) { mRightItems.append(item); } void Theme::Row::insertLeftItem(int idx, ContentItem *item) { if (idx >= mLeftItems.count()) { mLeftItems.append(item); return; } mLeftItems.insert(idx, item); } void Theme::Row::removeLeftItem(Theme::ContentItem *item) { mLeftItems.removeAll(item); } const QList &Theme::Row::rightItems() const { return mRightItems; } void Theme::Row::insertRightItem(int idx, ContentItem *item) { if (idx >= mRightItems.count()) { mRightItems.append(item); return; } mRightItems.insert(idx, item); } void Theme::Row::removeRightItem(Theme::ContentItem *item) { mRightItems.removeAll(item); } bool Theme::Row::containsTextItems() const { for (const auto ci : qAsConst(mLeftItems)) { if (ci->displaysText()) { return true; } } for (const auto ci : qAsConst(mRightItems)) { if (ci->displaysText()) { return true; } } return false; } void Theme::Row::save(QDataStream &stream) const { stream << (int)mLeftItems.count(); int cnt = mLeftItems.count(); for (int i = 0; i < cnt; ++i) { ContentItem *ci = mLeftItems.at(i); ci->save(stream); } stream << (int)mRightItems.count(); cnt = mRightItems.count(); for (int i = 0; i < cnt; ++i) { ContentItem *ci = mRightItems.at(i); ci->save(stream); } } bool Theme::Row::LoadContentItem(int val, QDataStream &stream, int themeVersion, bool leftItem) { if ((val < 0) || (val > 50)) { return false; // senseless } // FIXME: Remove code duplication here for (int i = 0; i < val; ++i) { ContentItem *ci = new ContentItem(ContentItem::Subject); // dummy type if (!ci->load(stream, themeVersion)) { qCDebug(MESSAGELIST_LOG) << "Left content item loading failed"; delete ci; return false; } if (leftItem) { addLeftItem(ci); } else { addRightItem(ci); } // Add the annotation item next to the attachment icon, so that users upgrading from old // versions don't manually need to set this. // Don't do this for the stand-alone attachment column. if (ci->type() == ContentItem::AttachmentStateIcon && themeVersion < gThemeMinimumVersionWithAnnotationIcon && val > 1) { qCDebug(MESSAGELIST_LOG) << "Old theme version detected, adding annotation item next to attachment icon."; ContentItem *annotationItem = new ContentItem(ContentItem::AnnotationIcon); annotationItem->setHideWhenDisabled(true); if (leftItem) { addLeftItem(annotationItem); } else { addRightItem(annotationItem); } } // Same as above, for the invitation icon if (ci->type() == ContentItem::AttachmentStateIcon && themeVersion < gThemeMinimumVersionWithInvitationIcon && val > 1) { qCDebug(MESSAGELIST_LOG) << "Old theme version detected, adding invitation item next to attachment icon."; ContentItem *invitationItem = new ContentItem(ContentItem::InvitationIcon); invitationItem->setHideWhenDisabled(true); if (leftItem) { addLeftItem(invitationItem); } else { addRightItem(invitationItem); } } } return true; } const QList &Theme::Row::leftItems() const { return mLeftItems; } bool Theme::Row::load(QDataStream &stream, int themeVersion) { removeAllLeftItems(); removeAllRightItems(); int val; // left item count stream >> val; if (!LoadContentItem(val, stream, themeVersion, true)) { return false; } // right item count stream >> val; if (!LoadContentItem(val, stream, themeVersion, false)) { return false; } return true; } Theme::Column::SharedRuntimeData::SharedRuntimeData(bool currentlyVisible, double currentWidth) : mReferences(0) , mCurrentlyVisible(currentlyVisible) , mCurrentWidth(currentWidth) { } Theme::Column::SharedRuntimeData::~SharedRuntimeData() { } void Theme::Column::SharedRuntimeData::addReference() { mReferences++; } bool Theme::Column::SharedRuntimeData::deleteReference() { mReferences--; Q_ASSERT(mReferences >= 0); return mReferences > 0; } int Theme::Column::SharedRuntimeData::referenceCount() const { return mReferences; } bool Theme::Column::SharedRuntimeData::currentlyVisible() const { return mCurrentlyVisible; } void Theme::Column::SharedRuntimeData::setCurrentlyVisible(bool visible) { mCurrentlyVisible = visible; } double Theme::Column::SharedRuntimeData::currentWidth() const { return mCurrentWidth; } void Theme::Column::SharedRuntimeData::setCurrentWidth(double currentWidth) { mCurrentWidth = currentWidth; } void Theme::Column::SharedRuntimeData::save(QDataStream &stream) const { stream << mCurrentlyVisible; stream << mCurrentWidth; } bool Theme::Column::SharedRuntimeData::load(QDataStream &stream, int /* themeVersion */) { stream >> mCurrentlyVisible; stream >> mCurrentWidth; if (mCurrentWidth > 10000) { qCDebug(MESSAGELIST_LOG) << "Theme has insane column width " << mCurrentWidth << " chopping to 100"; mCurrentWidth = 100; // avoid really insane values } return mCurrentWidth >= -1; } Theme::Column::Column() : mVisibleByDefault(true) , mIsSenderOrReceiver(false) , mMessageSorting(SortOrder::NoMessageSorting) { mSharedRuntimeData = new SharedRuntimeData(true, -1); mSharedRuntimeData->addReference(); } Theme::Column::Column(const Column &src) { mLabel = src.mLabel; mPixmapName = src.mPixmapName; mVisibleByDefault = src.mVisibleByDefault; mIsSenderOrReceiver = src.mIsSenderOrReceiver; mMessageSorting = src.mMessageSorting; mSharedRuntimeData = src.mSharedRuntimeData; mSharedRuntimeData->addReference(); for (const auto row : qAsConst(src.mMessageRows)) { addMessageRow(new Row(*row)); } for (const auto row : qAsConst(src.mGroupHeaderRows)) { addGroupHeaderRow(new Row(*row)); } } Theme::Column::~Column() { removeAllMessageRows(); removeAllGroupHeaderRows(); if (!(mSharedRuntimeData->deleteReference())) { delete mSharedRuntimeData; } } const QString &Theme::Column::label() const { return mLabel; } void Theme::Column::setLabel(const QString &label) { mLabel = label; } const QString &Theme::Column::pixmapName() const { return mPixmapName; } void Theme::Column::setPixmapName(const QString &pixmapName) { mPixmapName = pixmapName; } bool Theme::Column::isSenderOrReceiver() const { return mIsSenderOrReceiver; } void Theme::Column::setIsSenderOrReceiver(bool sor) { mIsSenderOrReceiver = sor; } bool Theme::Column::visibleByDefault() const { return mVisibleByDefault; } void Theme::Column::setVisibleByDefault(bool vbd) { mVisibleByDefault = vbd; } void Theme::Column::detach() { if (mSharedRuntimeData->referenceCount() < 2) { return; // nothing to detach } mSharedRuntimeData->deleteReference(); mSharedRuntimeData = new SharedRuntimeData(mVisibleByDefault, -1); mSharedRuntimeData->addReference(); } SortOrder::MessageSorting Theme::Column::messageSorting() const { return mMessageSorting; } void Theme::Column::setMessageSorting(SortOrder::MessageSorting ms) { mMessageSorting = ms; } bool Theme::Column::currentlyVisible() const { return mSharedRuntimeData->currentlyVisible(); } void Theme::Column::setCurrentlyVisible(bool currentlyVisible) { mSharedRuntimeData->setCurrentlyVisible(currentlyVisible); } double Theme::Column::currentWidth() const { return mSharedRuntimeData->currentWidth(); } void Theme::Column::setCurrentWidth(double currentWidth) { mSharedRuntimeData->setCurrentWidth(currentWidth); } const QList &Theme::Column::messageRows() const { return mMessageRows; } void Theme::Column::removeAllMessageRows() { while (!mMessageRows.isEmpty()) { delete mMessageRows.takeFirst(); } } void Theme::Column::addMessageRow(Theme::Row *row) { mMessageRows.append(row); } void Theme::Column::removeAllGroupHeaderRows() { while (!mGroupHeaderRows.isEmpty()) { delete mGroupHeaderRows.takeFirst(); } } void Theme::Column::addGroupHeaderRow(Theme::Row *row) { mGroupHeaderRows.append(row); } void Theme::Column::insertMessageRow(int idx, Row *row) { if (idx >= mMessageRows.count()) { mMessageRows.append(row); return; } mMessageRows.insert(idx, row); } void Theme::Column::removeMessageRow(Theme::Row *row) { mMessageRows.removeAll(row); } const QList &Theme::Column::groupHeaderRows() const { return mGroupHeaderRows; } void Theme::Column::insertGroupHeaderRow(int idx, Row *row) { if (idx >= mGroupHeaderRows.count()) { mGroupHeaderRows.append(row); return; } mGroupHeaderRows.insert(idx, row); } void Theme::Column::removeGroupHeaderRow(Theme::Row *row) { mGroupHeaderRows.removeAll(row); } bool Theme::Column::containsTextItems() const { for (const auto row : qAsConst(mMessageRows)) { if (row->containsTextItems()) { return true; } } for (const auto row : qAsConst(mGroupHeaderRows)) { if (row->containsTextItems()) { return true; } } return false; } void Theme::Column::save(QDataStream &stream) const { stream << mLabel; stream << mPixmapName; stream << mVisibleByDefault; stream << mIsSenderOrReceiver; stream << static_cast(mMessageSorting); stream << static_cast(mGroupHeaderRows.count()); int cnt = mGroupHeaderRows.count(); for (int i = 0; i < cnt; ++i) { Row *row = mGroupHeaderRows.at(i); row->save(stream); } cnt = mMessageRows.count(); stream << static_cast(cnt); for (int i = 0; i < cnt; ++i) { Row *row = mMessageRows.at(i); row->save(stream); } // added in version 0x1014 mSharedRuntimeData->save(stream); } bool Theme::Column::load(QDataStream &stream, int themeVersion) { removeAllGroupHeaderRows(); removeAllMessageRows(); stream >> mLabel; if (themeVersion >= gThemeMinimumVersionWithColumnIcon) { stream >> mPixmapName; } stream >> mVisibleByDefault; stream >> mIsSenderOrReceiver; int val; stream >> val; mMessageSorting = static_cast< SortOrder::MessageSorting >(val); if (!SortOrder::isValidMessageSorting(mMessageSorting)) { qCDebug(MESSAGELIST_LOG) << "Invalid message sorting"; return false; } if (themeVersion < gThemeMinimumVersionWithSortingByUnreadStatusAllowed) { // The default "Classic" theme "Unread" column had sorting disabled here. // We want to be nice to the existing users and automatically set // the new sorting method for this column (so they don't have to make the // complex steps to set it by themselves). // This piece of code isn't strictly required: it's just a niceness :) if ((mMessageSorting == SortOrder::NoMessageSorting) && (mLabel == i18n("Unread"))) { mMessageSorting = SortOrder::SortMessagesByUnreadStatus; } } // group header row count stream >> val; if ((val < 0) || (val > 50)) { qCDebug(MESSAGELIST_LOG) << "Invalid group header row count"; return false; // senseless } for (int i = 0; i < val; ++i) { Row *row = new Row(); if (!row->load(stream, themeVersion)) { qCDebug(MESSAGELIST_LOG) << "Group header row loading failed"; delete row; return false; } addGroupHeaderRow(row); } // message row count stream >> val; if ((val < 0) || (val > 50)) { qCDebug(MESSAGELIST_LOG) << "Invalid message row count"; return false; // senseless } for (int i = 0; i < val; ++i) { Row *row = new Row(); if (!row->load(stream, themeVersion)) { qCDebug(MESSAGELIST_LOG) << "Message row loading failed"; delete row; return false; } addMessageRow(row); } if (themeVersion >= gThemeMinimumVersionWithColumnRuntimeData) { // starting with version 0x1014 we have runtime data too if (!mSharedRuntimeData->load(stream, themeVersion)) { qCDebug(MESSAGELIST_LOG) << "Shared runtime data loading failed"; return false; } } else { // assume default shared data mSharedRuntimeData->setCurrentlyVisible(mVisibleByDefault); mSharedRuntimeData->setCurrentWidth(-1); } return true; } Theme::Theme() : OptionSet() { mGroupHeaderBackgroundMode = AutoColor; mViewHeaderPolicy = ShowHeaderAlways; mIconSize = gThemeDefaultIconSize; mGroupHeaderBackgroundStyle = StyledJoinedRect; } Theme::Theme(const QString &name, const QString &description, bool readOnly) : OptionSet(name, description, readOnly) { mGroupHeaderBackgroundMode = AutoColor; mGroupHeaderBackgroundStyle = StyledJoinedRect; mViewHeaderPolicy = ShowHeaderAlways; mIconSize = gThemeDefaultIconSize; } Theme::Theme(const Theme &src) : OptionSet(src) { mGroupHeaderBackgroundMode = src.mGroupHeaderBackgroundMode; mGroupHeaderBackgroundColor = src.mGroupHeaderBackgroundColor; mGroupHeaderBackgroundStyle = src.mGroupHeaderBackgroundStyle; mViewHeaderPolicy = src.mViewHeaderPolicy; mIconSize = src.mIconSize; for (const auto col : qAsConst(src.mColumns)) { addColumn(new Column(*col)); } } Theme::~Theme() { clearPixmapCache(); removeAllColumns(); } void Theme::detach() { for (const auto col : qAsConst(mColumns)) { col->detach(); } } void Theme::resetColumnState() { for (const auto col : qAsConst(mColumns)) { col->setCurrentlyVisible(col->visibleByDefault()); col->setCurrentWidth(-1); } } void Theme::resetColumnSizes() { for (const auto col : qAsConst(mColumns)) { col->setCurrentWidth(-1); } } const QList &Theme::columns() const { return mColumns; } Theme::Column *Theme::column(int idx) const { return mColumns.count() > idx ? mColumns.at(idx) : nullptr; } void Theme::removeAllColumns() { while (!mColumns.isEmpty()) { delete mColumns.takeFirst(); } } void Theme::addColumn(Theme::Column *column) { mColumns.append(column); } void Theme::insertColumn(int idx, Column *column) { if (idx >= mColumns.count()) { mColumns.append(column); return; } mColumns.insert(idx, column); } void Theme::removeColumn(Theme::Column *col) { mColumns.removeAll(col); } Theme::GroupHeaderBackgroundMode Theme::groupHeaderBackgroundMode() const { return mGroupHeaderBackgroundMode; } void Theme::moveColumn(int idx, int newPosition) { if ((newPosition >= mColumns.count()) || newPosition < 0) { return; } mColumns.move(idx, newPosition); } void Theme::setGroupHeaderBackgroundMode(GroupHeaderBackgroundMode bm) { mGroupHeaderBackgroundMode = bm; if ((bm == CustomColor) && !mGroupHeaderBackgroundColor.isValid()) { mGroupHeaderBackgroundColor = QColor(127, 127, 127); // something neutral } } const QColor &Theme::groupHeaderBackgroundColor() const { return mGroupHeaderBackgroundColor; } void Theme::setGroupHeaderBackgroundColor(const QColor &clr) { mGroupHeaderBackgroundColor = clr; } Theme::GroupHeaderBackgroundStyle Theme::groupHeaderBackgroundStyle() const { return mGroupHeaderBackgroundStyle; } void Theme::setGroupHeaderBackgroundStyle(Theme::GroupHeaderBackgroundStyle groupHeaderBackgroundStyle) { mGroupHeaderBackgroundStyle = groupHeaderBackgroundStyle; } QVector > Theme::enumerateViewHeaderPolicyOptions() { return { { i18n("Never Show"), NeverShowHeader }, { i18n("Always Show"), ShowHeaderAlways } }; } QVector > Theme::enumerateGroupHeaderBackgroundStyles() { return { { i18n("Plain Rectangles"), PlainRect }, { i18n("Plain Joined Rectangle"), PlainJoinedRect }, { i18n("Rounded Rectangles"), RoundedRect }, { i18n("Rounded Joined Rectangle"), RoundedJoinedRect }, { i18n("Gradient Rectangles"), GradientRect }, { i18n("Gradient Joined Rectangle"), GradientJoinedRect }, { i18n("Styled Rectangles"), StyledRect }, { i18n("Styled Joined Rectangles"), StyledJoinedRect } }; } Theme::ViewHeaderPolicy Theme::viewHeaderPolicy() const { return mViewHeaderPolicy; } void Theme::setViewHeaderPolicy(Theme::ViewHeaderPolicy vhp) { mViewHeaderPolicy = vhp; } int Theme::iconSize() const { return mIconSize; } void Theme::setIconSize(int iconSize) { if (mIconSize != iconSize) { clearPixmapCache(); mIconSize = iconSize; if ((mIconSize < 8) || (mIconSize > 64)) { mIconSize = gThemeDefaultIconSize; } } } bool Theme::load(QDataStream &stream) { removeAllColumns(); int themeVersion; stream >> themeVersion; // We support themes starting at version gThemeMinimumSupportedVersion (0x1013 actually) if ( (themeVersion > gThemeCurrentVersion) || (themeVersion < gThemeMinimumSupportedVersion) ) { qCDebug(MESSAGELIST_LOG) << "Invalid theme version"; return false; // b0rken (invalid version) } int val; stream >> val; mGroupHeaderBackgroundMode = static_cast(val); switch (mGroupHeaderBackgroundMode) { case Transparent: case AutoColor: case CustomColor: // ok break; default: qCDebug(MESSAGELIST_LOG) << "Invalid theme group header background mode"; return false; // b0rken } stream >> mGroupHeaderBackgroundColor; stream >> val; mGroupHeaderBackgroundStyle = static_cast(val); switch (mGroupHeaderBackgroundStyle) { case PlainRect: case PlainJoinedRect: case RoundedRect: case RoundedJoinedRect: case GradientRect: case GradientJoinedRect: case StyledRect: case StyledJoinedRect: // ok break; default: qCDebug(MESSAGELIST_LOG) << "Invalid theme group header background style"; return false; // b0rken } stream >> val; mViewHeaderPolicy = (ViewHeaderPolicy)val; switch (mViewHeaderPolicy) { case ShowHeaderAlways: case NeverShowHeader: // ok break; default: qCDebug(MESSAGELIST_LOG) << "Invalid theme view header policy"; return false; // b0rken } if (themeVersion >= gThemeMinimumVersionWithIconSizeField) { // icon size parameter stream >> mIconSize; if ((mIconSize < 8) || (mIconSize > 64)) { mIconSize = gThemeDefaultIconSize; // limit insane values } } else { mIconSize = gThemeDefaultIconSize; } // column count stream >> val; if (val < 1 || val > 50) { return false; // plain b0rken ( negative, zero or more than 50 columns ) } for (int i = 0; i < val; ++i) { Column *col = new Column(); if (!col->load(stream, themeVersion)) { qCDebug(MESSAGELIST_LOG) << "Column loading failed"; delete col; return false; } addColumn(col); } return true; } void Theme::save(QDataStream &stream) const { stream << (int)gThemeCurrentVersion; stream << (int)mGroupHeaderBackgroundMode; stream << mGroupHeaderBackgroundColor; stream << (int)mGroupHeaderBackgroundStyle; stream << (int)mViewHeaderPolicy; stream << mIconSize; const int cnt = mColumns.count(); stream << (int)cnt; for (int i = 0; i < cnt; ++i) { Column *col = mColumns.at(i); col->save(stream); } } void Theme::clearPixmapCache() const { qDeleteAll(mPixmaps); mPixmaps.clear(); } void Theme::populatePixmapCache() const { clearPixmapCache(); mPixmaps.reserve(_IconCount); // WARNING: The order of those icons must be in sync with order of the // corresponding enum values in ThemeIcon! mPixmaps << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-unread-new")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-unread")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-read")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-deleted")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-replied")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-forwarded-replied")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-queued")).pixmap(mIconSize, mIconSize)) // mail-queue ? << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-task")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-sent")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-forwarded")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-important")).pixmap(mIconSize, mIconSize)) // "flag" << new QPixmap(QIcon::fromTheme(QStringLiteral("messagelist/pics/mail-thread-watch.png")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("messagelist/pics/mail-thread-ignored.png")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-junk")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-notjunk")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed-verified")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed-part")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted-full")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted-part")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-attachment")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("view-pim-notes")).pixmap(mIconSize, mIconSize)) << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-invitation")).pixmap(mIconSize, mIconSize)) << ((QApplication::isRightToLeft()) ? new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-left")).pixmap(mIconSize, mIconSize)) : new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-right")).pixmap(mIconSize, mIconSize))) << new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-down")).pixmap(mIconSize, mIconSize)) << new QPixmap(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-vertical-separator-line.png"))) << new QPixmap(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-horizontal-space.png"))); } diff --git a/messagelist/src/core/theme.h b/messagelist/src/core/theme.h index 45fd9ea4..7ca80c91 100644 --- a/messagelist/src/core/theme.h +++ b/messagelist/src/core/theme.h @@ -1,1024 +1,1028 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef MESSAGELIST_CORE_THEME_H #define MESSAGELIST_CORE_THEME_H #include #include #include #include #include #include #include class QPixmap; namespace MessageList { namespace Core { /** * The Theme class defines the visual appearance of the MessageList. * * The core structure of the theme is made up of Column objects which * are mapped to View (QTreeView) columns. Each theme must provide at least one column. * * Each column contains a set of Row objects dedicated to message items * and a set of Row objects dedicated to group header items. There must be at least * one message row and one group header row in each column. Rows are visually * ordered from top to bottom. * * Each Row contains a set of left aligned and a set of right aligned ContentItem objects. * The right aligned items are painted from right to left while the left aligned * ones are painted from left to right. In Right-To-Left mode the ThemeDelegate * follows the exact opposite convention. * * Each ContentItem object specifies a visual element to be displayed in a View * row. The visual elements may be pieces of text (Subject, Date) or icons. * * The Theme is designed to strictly interoperate with the ThemeDelegate class * which takes care of rendering the contents when attached to an QAbstractItemView. */ class Theme : public OptionSet { public: /** * The ContentItem class defines a content item inside a Row. * Content items are data items extracted from a message or a group header: * they can be text, spacers, separators or icons. */ class ContentItem { private: /** * Bits for composing the Type enumeration members. * We'll be able to test these bits to quickly figure out item properties. */ enum TypePropertyBits { /** * Item can use the custom color property */ CanUseCustomColor = (1 << 16), /** * Item can be in a disabled state (for example the attachment icon when there is no attachment) */ CanBeDisabled = (1 << 17), /** * Item displays some sort of text */ DisplaysText = (1 << 18), /** * Item makes sense (and can be applied) for messages */ ApplicableToMessageItems = (1 << 19), /** * Item makes sense (and can be applied) for group headers */ ApplicableToGroupHeaderItems = (1 << 20), /** * The item takes more horizontal space than the other text items (at the time of writing it's only the subject) */ LongText = (1 << 21), /** * The item displays an icon */ IsIcon = (1 << 22), /** * The item is a small spacer */ IsSpacer = (1 << 23), /** * The item is clickable */ IsClickable = (1 << 24) }; public: /** * The available ContentItem types. * Note that the values in this enum are unique values or'ed with the TypePropertyBits above. */ enum Type { /** * Display the subject of the message item. This is a long text. */ Subject = 1 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems | LongText, /** * Formatted date time of the message/group */ Date = 2 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems | ApplicableToGroupHeaderItems, /** * From: or To: strip, depending on the folder settings */ SenderOrReceiver = 3 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems, /** * From: strip, always */ Sender = 4 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems, /** * To: strip, always */ Receiver = 5 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems, /** * Formatted size of the message */ Size = 6 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems, /** * The icon that displays the unread/read state (never disabled) */ ReadStateIcon = 7 | ApplicableToMessageItems | IsIcon, /** * The icon that displays the attachment state (may be disabled) */ AttachmentStateIcon = 8 | CanBeDisabled | ApplicableToMessageItems | IsIcon, /** * The icon that displays the replied/forwarded state (may be disabled) */ RepliedStateIcon = 9 | CanBeDisabled | ApplicableToMessageItems | IsIcon, /** * The group header label */ GroupHeaderLabel = 10 | DisplaysText | CanUseCustomColor | ApplicableToGroupHeaderItems, /** * The ActionItem state icon. May be disabled. Clickable (cycles todo->nothing) */ ActionItemStateIcon = 11 | CanBeDisabled | ApplicableToMessageItems | IsIcon | IsClickable, /** * The Important tag icon. May be disabled. Clickable (cycles important->nothing) */ ImportantStateIcon = 12 | CanBeDisabled | ApplicableToMessageItems | IsIcon | IsClickable, /** * The Spam/Ham state icon. May be disabled. Clickable (cycles spam->ham->nothing) */ SpamHamStateIcon = 13 | CanBeDisabled | ApplicableToMessageItems | IsIcon | IsClickable, /** * The Watched/Ignored state icon. May be disabled. Clickable (cycles watched->ignored->nothing) */ WatchedIgnoredStateIcon = 14 | CanBeDisabled | ApplicableToMessageItems | IsIcon | IsClickable, /** * The Expanded state icon for group headers. May be disabled. Clickable (expands/collapses the group) */ ExpandedStateIcon = 15 | CanBeDisabled | ApplicableToGroupHeaderItems | IsIcon | IsClickable, /** * The Encryption state icon for messages. May be disabled (no encryption). */ EncryptionStateIcon = 16 | CanBeDisabled | ApplicableToMessageItems | IsIcon, /** * The Signature state icon for messages. May be disabled (no signature) */ SignatureStateIcon = 17 | CanBeDisabled | ApplicableToMessageItems | IsIcon, /** * A vertical separation line. */ VerticalLine = 18 | CanUseCustomColor | ApplicableToMessageItems | ApplicableToGroupHeaderItems | IsSpacer, /** * A small empty spacer usable as separator. */ HorizontalSpacer = 19 | ApplicableToMessageItems | ApplicableToGroupHeaderItems | IsSpacer, /** * The date of the most recent message in subtree */ MostRecentDate = 20 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems | ApplicableToGroupHeaderItems, /** * The combined icon that displays the unread/read/replied/forwarded state (never disabled) */ CombinedReadRepliedStateIcon = 21 | ApplicableToMessageItems | IsIcon, /** * The list of MessageItem::Tag entries */ TagList = 22 | ApplicableToMessageItems | IsIcon, /** * Whether the message has a annotation/note */ AnnotationIcon = 23 | ApplicableToMessageItems | IsIcon | CanBeDisabled | IsClickable, /** * Whether the message is an invitation */ - InvitationIcon = 24 | ApplicableToMessageItems | IsIcon + InvitationIcon = 24 | ApplicableToMessageItems | IsIcon, + /** + * Folder of the message + */ + Folder = 25 | DisplaysText | CanUseCustomColor | ApplicableToMessageItems #if 0 TotalMessageCount UnreadMessageCount NewMessageCount #endif }; enum Flags { HideWhenDisabled = 1, ///< In disabled state the icon should take no space (overrides SoftenByBlendingWhenDisabled) SoftenByBlendingWhenDisabled = (1 << 1), ///< In disabled state the icon should be still shown, but made very soft by alpha blending UseCustomColor = (1 << 2), ///< For text and vertical line. If set then always use a custom color, otherwise use default text color IsBold = (1 << 3), ///< For text items. If set then always show as bold, otherwise use the default font weight IsItalic = (1 << 4), ///< Fot text items. If set then always show as italic, otherwise use the default font style SoftenByBlending = (1 << 5) ///< For text items: use 60% opacity. }; private: Type mType; ///< The type of item unsigned int mFlags; ///< The flags of the item QColor mCustomColor; ///< The color to use with this content item, meaningful only if canUseCustomColor() return true. public: /** * Creates a ContentItem with the specified type. * A content item must be added to a theme Row. */ explicit ContentItem(Type type); /** * Creates a ContentItem that is a copy of the content item src. * A content item must be added to a theme Row. */ explicit ContentItem(const ContentItem &src); public: /** * Returns the type of this content item */ Type type() const; /** * Returns true if this ContentItem can be in a "disabled" state. * The attachment state icon, for example, can be disabled when the related * message has no attachments. For such items the HideWhenDisabled * and SoftenByBlendingWhenDisabled flags are meaningful. */ bool canBeDisabled() const; /** * Returns true if this ContentItem can make use of a custom color. */ bool canUseCustomColor() const; /** * Returns true if this item displays some kind of text. * Items that display text make use of the customFont() setting. */ bool displaysText() const; /** * Returns true if this item displays a long text. * The returned value makes sense only if displaysText() returned true. */ bool displaysLongText() const; /** * Returns true if this item displays an icon. */ bool isIcon() const; /** * Returns true if clicking on this kind of item can perform an action */ bool isClickable() const; /** * Returns true if this item is a small spacer */ bool isSpacer() const; /** * Static test that returns true if an instance of ContentItem with the * specified type makes sense in a Row for message items. */ static bool applicableToMessageItems(Type type); /** * Static test that returns true if an instance of ContentItem with the * specified type makes sense in a Row for group header items. */ static bool applicableToGroupHeaderItems(Type type); /** * Returns a descriptive name for the specified content item type */ static QString description(Type type); /** * Returns true if this item uses a custom color. * The return value of this function is valid only if canUseCustomColor() returns true. */ bool useCustomColor() const; /** * Makes this item use the custom color that can be set by setCustomColor(). * The custom color is meaningful only if canUseCustomColor() returns true. */ void setUseCustomColor(bool useCustomColor); /** * Returns true if this item uses a bold text. * The return value of this function is valid only if displaysText() returns true. */ bool isBold() const; /** * Makes this item use a bold font. */ void setBold(bool isBold); /** * Returns true if this item uses an italic text. * The return value of this function is valid only if displaysText() returns true. */ bool isItalic() const; /** * Makes this item use italic font. */ void setItalic(bool isItalic); /** * Returns true if this item should be hidden when in disabled state. * Hidden content items simply aren't painted and take no space. * This flag has meaning only on items for that canBeDisabled() returns true. */ bool hideWhenDisabled() const; /** * Sets the flag that causes this item to be hidden when disabled. * Hidden content items simply aren't painted and take no space. * This flag overrides the setSoftenByBlendingWhenDisabled() setting. * This flag has meaning only on items for that canBeDisabled() returns true. */ void setHideWhenDisabled(bool hideWhenDisabled); /** * Returns true if this item should be painted in a "soft" fashion when * in disabled state. Soft icons are painted with very low opacity. * This flag has meaning only on items for that canBeDisabled() returns true. */ bool softenByBlendingWhenDisabled() const; /** * Sets the flag that causes this item to be painted "softly" when disabled. * Soft icons are painted with very low opacity. * This flag may be overridden by the setHideWhenDisabled() setting. * This flag has meaning only on items for that canBeDisabled() returns true. */ void setSoftenByBlendingWhenDisabled(bool softenByBlendingWhenDisabled); /** * Returns true if this item should be always painted in a "soft" fashion. * Meaningful only for text items. */ bool softenByBlending() const; /** * Sets the flag that causes this item to be painted "softly". * Meaningful only for text items. */ void setSoftenByBlending(bool softenByBlending); /** * Returns the custom color set for this item. * The return value is meaningful only if canUseCustomColor() returns true * returns true and setUseCustomColor( true ) has been called. */ const QColor &customColor() const; /** * Sets the custom color for this item. Meaningful only if canUseCustomColor() * returns true and you call setUseCustomColor( true ) */ void setCustomColor(const QColor &clr); // Stuff used by ThemeDelegate. This section should be protected but some gcc // versions seem to get confused with nested class and friend declarations // so for portability we're using a public interface also here. /** * Handles content item saving (used by Theme::Row::save()) */ void save(QDataStream &stream) const; /** * Handles content item loading (used by Theme::Row::load()) */ bool load(QDataStream &stream, int themeVersion); }; /** * The Row class defines a row of items inside a Column. * The Row has a list of left aligned and a list of right aligned ContentItems. */ class Row { public: explicit Row(); explicit Row(const Row &src); ~Row(); private: QList< ContentItem * > mLeftItems; ///< The list of left aligned items QList< ContentItem * > mRightItems; ///< The list of right aligned items bool LoadContentItem(int val, QDataStream &stream, int themeVersion, bool leftItem); public: /** * Returns the list of left aligned items for this row */ const QList< ContentItem * > &leftItems() const; /** * Removes all the left items from this row: the items are deleted. */ void removeAllLeftItems(); /** * Adds a left aligned item to this row. The row takes the ownership * of the ContentItem pointer. */ void addLeftItem(ContentItem *item); /** * Adds a left aligned item at the specified position in this row. The row takes the ownership * of the ContentItem pointer. */ void insertLeftItem(int idx, ContentItem *item); /** * Removes the specified left aligned content item from this row. * The item is NOT deleted. */ void removeLeftItem(ContentItem *item); /** * Returns the list of right aligned items for this row */ const QList< ContentItem * > &rightItems() const; /** * Removes all the right items from this row. The items are deleted. */ void removeAllRightItems(); /** * Adds a right aligned item to this row. The row takes the ownership * of the ContentItem pointer. Please note that the first right aligned item * will start at the right edge, the second right aligned item will come after it etc... */ void addRightItem(ContentItem *item); /** * Adds a right aligned item at the specified position in this row. The row takes the ownership * of the ContentItem pointer. Remember that right item positions go from right to left. */ void insertRightItem(int idx, ContentItem *item); /** * Removes the specified right aligned content item from this row. * The item is NOT deleted. */ void removeRightItem(ContentItem *item); /** * Returns true if this row contains text items. * This is useful if you want to know if the column should just get * its minimum allowable space or it should get more. */ bool containsTextItems() const; /** * Handles row saving (used by Theme::Column::save()) */ void save(QDataStream &stream) const; /** * Handles row loading (used by Theme::Column::load()) */ bool load(QDataStream &stream, int themeVersion); }; /** * The Column class defines a view column available inside this theme. * Each Column has a list of Row items that define the visible rows. */ class Column { public: /** * A set of shared runtime data. This is used to store a set of "override" settings * at runtime. For instance, the width of the visible columns of a skin are stored here. */ class SharedRuntimeData { private: int mReferences; ///< The number of external references to this shared data object int mCurrentlyVisible; ///< Is this column currently visible ? always valid (eventually set from default) double mCurrentWidth; ///< The current width of this column, -1 if not valid (never set) public: /** * Create a shared runtime data object */ explicit SharedRuntimeData(bool currentlyVisible, double currentWidth); /** * Destroy a shared runtime data object */ ~SharedRuntimeData(); public: /** * Increments the reference count for this shared runtime data object. */ void addReference(); /** * Decrements the reference count for this shared runtime data object. * Returns true if there are other references and false otherwise (so the data can be safely deleted) */ bool deleteReference(); /** * Returns the current number of reference counts, that is, the number of * Theme::Column objects that use this SharedRuntimeData instance. */ int referenceCount() const; /** * Returns the current visibility state */ bool currentlyVisible() const; /** * Sets the current visibility state */ void setCurrentlyVisible(bool visible); /** * Returns the current width or -1 if the width is unspecified/invalid */ double currentWidth() const; /** * Sets the current width of the column */ void setCurrentWidth(double currentWidth); /** * Saves this runtime data to the specified stream */ void save(QDataStream &stream) const; /** * Loads the shared runtime data from the specified stream * assuming that it uses the specified theme version. * Returns true on success and false if the data can't be loaded. */ bool load(QDataStream &stream, int themeVersion); }; public: /** * Create an empty column with default settings */ explicit Column(); /** * Create an exact copy of the column src. * The shared runtime data is not copied (only a reference is added). * If you need to create an independent clone then please use detach() * after the construction. */ explicit Column(const Column &src); /** * Kill a column object */ ~Column(); private: QString mLabel; ///< The label visible in the column header QString mPixmapName; ///< The icon's name visible in the column header if it was set bool mVisibleByDefault; ///< Is this column visible by default ? bool mIsSenderOrReceiver; ///< If this column displays the sender/receiver field then we will update its label on the fly SortOrder::MessageSorting mMessageSorting; ///< The message sort order we switch to when clicking on this column QList< Row * > mGroupHeaderRows; ///< The list of rows we display in this column for a GroupHeaderItem QList< Row * > mMessageRows; ///< The list of rows we display in this column for a MessageItem SharedRuntimeData *mSharedRuntimeData = nullptr; ///< A pointer to the shared runtime data: shared between all instances of a theme with the same id public: /** * Returns the label set for this column */ const QString &label() const; /** * Sets the label for this column */ void setLabel(const QString &label); /** * Returns the icon's name (used in SmallIcon) set for this column */ const QString &pixmapName() const; /** * Sets the icon's name (used in SmallIcon) for this column */ void setPixmapName(const QString &pixmapName); /** * Returns true if this column is marked as "sender/receiver" and we should * update its label on-the-fly. */ bool isSenderOrReceiver() const; /** * Marks this column as containing the "sender/receiver" field. * Such columns will have the label automatically updated. */ void setIsSenderOrReceiver(bool sor); /** * Returns true if this column has to be shown by default */ bool visibleByDefault() const; /** * Sets the "visible by default" tag for this column. */ void setVisibleByDefault(bool vbd); /** * Detaches the shared runtime data object and makes this object * totally independent. The shared runtime data is initialized to default values. */ void detach(); /** * Returns the sort order for messages that we should switch to * when clicking on this column's header (if visible at all). */ SortOrder::MessageSorting messageSorting() const; /** * Sets the sort order for messages that we should switch to * when clicking on this column's header (if visible at all). */ void setMessageSorting(SortOrder::MessageSorting ms); /** * Returns the current shared visibility state for this column. * This state is shared between all the instances of this theme. */ bool currentlyVisible() const; /** * Sets the current shared visibility state for this column. * This state is shared between all the instances of this theme. */ void setCurrentlyVisible(bool currentlyVisible); /** * Returns the current shared width setting for this column * or -1 if the width is not specified and should be auto-determined. * This state is shared between all the instances of this theme. */ double currentWidth() const; /** * Sets the current shared width setting for this column. * This state is shared between all the instances of this theme. */ void setCurrentWidth(double currentWidth); /** * Returns the list of rows visible in this column for a MessageItem */ const QList< Row * > &messageRows() const; /** * Removes all the message rows from this column. */ void removeAllMessageRows(); /** * Appends a message row to this theme column. The Theme takes * the ownership of the Row pointer. */ void addMessageRow(Row *row); /** * Inserts a message row to this theme column in the specified position. The Theme takes * the ownership of the Row pointer. */ void insertMessageRow(int idx, Row *row); /** * Removes the specified message row. The row is NOT deleted. */ void removeMessageRow(Row *row); /** * Returns the list of rows visible in this column for a GroupHeaderItem */ const QList< Row * > &groupHeaderRows() const; /** * Removes all the group header rows from this column. */ void removeAllGroupHeaderRows(); /** * Appends a group header row to this theme. The Theme takes * the ownership of the Row pointer. */ void addGroupHeaderRow(Row *row); /** * Inserts a group header row to this theme column in the specified position. The Theme takes * the ownership of the Row pointer. */ void insertGroupHeaderRow(int idx, Row *row); /** * Removes the specified group header row. The row is NOT deleted. */ void removeGroupHeaderRow(Row *row); /** * Returns true if this column contains text items. * This is useful if you want to know if the column should just get * its minimum allowable space or it should get more. */ bool containsTextItems() const; /** * Handles column saving (used by Theme::save()) */ void save(QDataStream &stream) const; /** * Handles column loading (used by Theme::load()) */ bool load(QDataStream &stream, int themeVersion); }; public: /** * Creates a totally uninitialized theme object. */ explicit Theme(); /** * Creates a theme object with the specified name and description. */ explicit Theme(const QString &name, const QString &description, bool readOnly = false); /** * Creates an exact copy of the theme sharing the same runtime data. * If you need an exact clone please use detach() and generateUniqueId() just * after creation. */ explicit Theme(const Theme &src); /** * Destroys this theme object. */ ~Theme(); static bool compareName(Theme *theme1, Theme *theme2) { return theme1->name() < theme2->name(); } public: /** * Which color do we use to paint group header background ? */ enum GroupHeaderBackgroundMode { Transparent, ///< No background at all: use style default AutoColor, ///< Automatically determine the color (somewhere in the middle between background and text) CustomColor ///< Use a custom color }; /** * How do we paint group header background ? */ enum GroupHeaderBackgroundStyle { PlainRect, ///< One plain rect per column PlainJoinedRect, ///< One big plain rect for all the columns RoundedRect, ///< One rounded rect per column RoundedJoinedRect, ///< One big rounded rect for all the columns GradientRect, ///< One rounded gradient filled rect per column GradientJoinedRect, ///< One big rounded gradient rect for all the columns StyledRect, ///< One styled rect per column StyledJoinedRect ///< One big styled rect per column }; /** * How do we manage the QHeaderView attached to our View ? */ enum ViewHeaderPolicy { ShowHeaderAlways, NeverShowHeader //ShowWhenMoreThanOneColumn, ///< This doesn't work at the moment (since without header we don't have means for showing columns back) }; enum ThemeIcon { IconNew, IconUnread, IconRead, IconDeleted, IconReplied, IconRepliedAndForwarded, IconQueued, IconActionItem, IconSent, IconForwarded, IconImportant, IconWatched, IconIgnored, IconSpam, IconHam, IconFullySigned, IconPartiallySigned, IconUndefinedSigned, IconNotSigned, IconFullyEncrypted, IconPartiallyEncrypted, IconUndefinedEncrypted, IconNotEncrypted, IconAttachment, IconAnnotation, IconInvitation, IconShowMore, IconShowLess, IconVerticalLine, IconHorizontalSpacer, _IconCount }; private: QList< Column * > mColumns; ///< The list of columns available in this theme // pixmaps cache. Mutable, so it can be lazily populated from const methods mutable QVector mPixmaps; GroupHeaderBackgroundMode mGroupHeaderBackgroundMode; ///< How do we paint group header background ? QColor mGroupHeaderBackgroundColor; ///< The background color of the message group, used only if CustomColor GroupHeaderBackgroundStyle mGroupHeaderBackgroundStyle; ///< How do we paint group header background ? ViewHeaderPolicy mViewHeaderPolicy; ///< Do we show the header or not ? int mIconSize; ///< The icon size for this theme, 16 is the default public: /** * Detaches this object from the shared runtime data for columns. */ void detach(); /** * Resets the column state (visibility and width) to their default values (the "visible by default" ones). */ void resetColumnState(); /** * Resets the column sizes to "default" (subset of resetColumnState() above). */ void resetColumnSizes(); /** * Returns the list of columns available in this theme */ const QList< Column * > &columns() const; /** * Returns a pointer to the column at the specified index or 0 if there is no such column */ Column *column(int idx) const; void moveColumn(int idx, int newPosition); /** * Removes all columns from this theme */ void removeAllColumns(); /** * Appends a column to this theme */ void addColumn(Column *column); /** * Inserts a column to this theme at the specified position. */ void insertColumn(int idx, Column *column); /** * Removes the specified message row. The row is NOT deleted. */ void removeColumn(Column *col); /** * Returns the group header background mode for this theme. */ GroupHeaderBackgroundMode groupHeaderBackgroundMode() const; /** * Sets the group header background mode for this theme. * If you set it to CustomColor then please also setGroupHeaderBackgroundColor() */ void setGroupHeaderBackgroundMode(GroupHeaderBackgroundMode bm); /** * Returns the group header background color for this theme. * This color is used only if groupHeaderBackgroundMode() is set to CustomColor. */ const QColor &groupHeaderBackgroundColor() const; /** * Sets the group header background color for this theme. * This color is used only if groupHeaderBackgroundMode() is set to CustomColor. */ void setGroupHeaderBackgroundColor(const QColor &clr); /** * Returns the group header background style for this theme. * The group header background style makes sense only if groupHeaderBackgroundMode() is * set to something different than Transparent. */ GroupHeaderBackgroundStyle groupHeaderBackgroundStyle() const; /** * Sets the group header background style for this theme. * The group header background style makes sense only if groupHeaderBackgroundMode() is * set to something different than Transparent. */ void setGroupHeaderBackgroundStyle(GroupHeaderBackgroundStyle groupHeaderBackgroundStyle); /** * Enumerates the available group header background styles. * The returned descriptors are pairs in that the first item is the localized description * of the option value and the second item is the integer option value itself. */ static QVector< QPair< QString, int > > enumerateGroupHeaderBackgroundStyles(); /** * Returns the currently set ViewHeaderPolicy */ ViewHeaderPolicy viewHeaderPolicy() const; /** * Sets the ViewHeaderPolicy for this theme */ void setViewHeaderPolicy(ViewHeaderPolicy vhp); /** * Returns the currently set icon size */ int iconSize() const; /** * Sets the icon size for this theme. * Please note that the function will not let you set insane values. * The allowable range is [8,64] */ void setIconSize(int iconSize); /** * Enumerates the available view header policy options. * The returned descriptors are pairs in that the first item is the localized description * of the option value and the second item is the integer option value itself. */ static QVector > enumerateViewHeaderPolicyOptions(); inline const QPixmap *pixmap(ThemeIcon icon) const { if (Q_UNLIKELY(mPixmaps.isEmpty())) { populatePixmapCache(); } return mPixmaps[icon]; } protected: /** * Pure virtual reimplemented from OptionSet. */ void save(QDataStream &stream) const override; /** * Pure virtual reimplemented from OptionSet. */ bool load(QDataStream &stream) override; void clearPixmapCache() const; void populatePixmapCache() const; }; } // namespace Core } // namespace MessageList #endif //!__MESSAGELIST_CORE_SKIN_H diff --git a/messagelist/src/core/themedelegate.cpp b/messagelist/src/core/themedelegate.cpp index 145c4762..2965c31a 100644 --- a/messagelist/src/core/themedelegate.cpp +++ b/messagelist/src/core/themedelegate.cpp @@ -1,1720 +1,1732 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free softhisare; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Softhisare Foundation; either version 2 of the License, or * (at your option) any later version. * * 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, write to the Free Softhisare * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/themedelegate.h" #include "core/messageitem.h" #include "core/groupheaderitem.h" #include "core/manager.h" #include "messagelistsettings.h" #include "MessageCore/StringUtil" #include "MessageCore/MessageCoreSettings" #include #include #include #include #include #include #include #include #include using namespace MessageList::Core; static const int gGroupHeaderOuterVerticalMargin = 1; static const int gGroupHeaderOuterHorizontalMargin = 1; static const int gGroupHeaderInnerVerticalMargin = 1; static const int gGroupHeaderInnerHorizontalMargin = 1; static const int gMessageVerticalMargin = 2; static const int gMessageHorizontalMargin = 2; static const int gHorizontalItemSpacing = 2; ThemeDelegate::ThemeDelegate(QAbstractItemView *parent) : QStyledItemDelegate(parent) , mTheme(nullptr) { mItemView = parent; } ThemeDelegate::~ThemeDelegate() { } void ThemeDelegate::setTheme(const Theme *theme) { mTheme = theme; if (!mTheme) { return; // hum } // Rebuild the group header background color cache switch (mTheme->groupHeaderBackgroundMode()) { case Theme::Transparent: mGroupHeaderBackgroundColor = QColor(); // invalid break; case Theme::CustomColor: mGroupHeaderBackgroundColor = mTheme->groupHeaderBackgroundColor(); break; case Theme::AutoColor: { QPalette pal = mItemView->palette(); QColor txt = pal.color(QPalette::Normal, QPalette::Text); QColor bck = pal.color(QPalette::Normal, QPalette::Base); mGroupHeaderBackgroundColor = QColor( (txt.red() + (bck.red() * 3)) / 4, (txt.green() + (bck.green() * 3)) / 4, (txt.blue() + (bck.blue() * 3)) / 4 ); break; } } generalFontChanged(); mItemView->reset(); } enum FontType { Normal, Bold, Italic, BoldItalic, FontTypesCount }; static QFont sFontCache[FontTypesCount]; static QFontMetrics sFontMetricsCache[FontTypesCount] = { QFontMetrics(QFont()), QFontMetrics(QFont()), QFontMetrics(QFont()), QFontMetrics(QFont()) }; static int sFontHeightCache = 0; static inline const QFontMetrics &cachedFontMetrics(const Theme::ContentItem *ci) { return (!ci->isBold() && !ci->isItalic()) ? sFontMetricsCache[Normal] : (ci->isBold() && !ci->isItalic()) ? sFontMetricsCache[Bold] : (!ci->isBold() && ci->isItalic()) ? sFontMetricsCache[Italic] : sFontMetricsCache[BoldItalic]; } static inline const QFont &cachedFont(const Theme::ContentItem *ci) { return (!ci->isBold() && !ci->isItalic()) ? sFontCache[Normal] : (ci->isBold() && !ci->isItalic()) ? sFontCache[Bold] : (!ci->isBold() && ci->isItalic()) ? sFontCache[Italic] : sFontCache[BoldItalic]; } static inline const QFont &cachedFont(const Theme::ContentItem *ci, const Item *i) { if (i->type() != Item::Message) { return cachedFont(ci); } const MessageItem *mi = static_cast(i); const bool bold = ci->isBold() || mi->isBold(); const bool italic = ci->isItalic() || mi->isItalic(); return (!bold && !italic) ? sFontCache[Normal] : (bold && !italic) ? sFontCache[Bold] : (!bold && italic) ? sFontCache[Italic] : sFontCache[BoldItalic]; } static inline void paint_right_aligned_elided_text(const QString &text, Theme::ContentItem *ci, QPainter *painter, int &left, int top, int &right, Qt::LayoutDirection layoutDir, const QFont &font) { painter->setFont(font); const QFontMetrics &fontMetrics = cachedFontMetrics(ci); const int w = right - left; const QString elidedText = fontMetrics.elidedText(text, layoutDir == Qt::LeftToRight ? Qt::ElideLeft : Qt::ElideRight, w); const QRect rct(left, top, w, sFontHeightCache); QRect outRct; if (ci->softenByBlending()) { qreal oldOpacity = painter->opacity(); painter->setOpacity(0.6); painter->drawText(rct, Qt::AlignTop | Qt::AlignRight | Qt::TextSingleLine, elidedText, &outRct); painter->setOpacity(oldOpacity); } else { painter->drawText(rct, Qt::AlignTop | Qt::AlignRight | Qt::TextSingleLine, elidedText, &outRct); } if (layoutDir == Qt::LeftToRight) { right -= outRct.width() + gHorizontalItemSpacing; } else { left += outRct.width() + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_right_aligned_elided_text(const QString &text, Theme::ContentItem *ci, int &left, int top, int &right, QRect &outRect, Qt::LayoutDirection layoutDir, const QFont &font) { Q_UNUSED(font); const QFontMetrics &fontMetrics = cachedFontMetrics(ci); const int w = right - left; const QString elidedText = fontMetrics.elidedText(text, layoutDir == Qt::LeftToRight ? Qt::ElideLeft : Qt::ElideRight, w); const QRect rct(left, top, w, sFontHeightCache); const Qt::AlignmentFlag af = layoutDir == Qt::LeftToRight ? Qt::AlignRight : Qt::AlignLeft; outRect = fontMetrics.boundingRect(rct, Qt::AlignTop | af | Qt::TextSingleLine, elidedText); if (layoutDir == Qt::LeftToRight) { right -= outRect.width() + gHorizontalItemSpacing; } else { left += outRect.width() + gHorizontalItemSpacing; } } static inline void paint_left_aligned_elided_text(const QString &text, Theme::ContentItem *ci, QPainter *painter, int &left, int top, int &right, Qt::LayoutDirection layoutDir, const QFont &font) { painter->setFont(font); const QFontMetrics &fontMetrics = cachedFontMetrics(ci); const int w = right - left; const QString elidedText = fontMetrics.elidedText(text, layoutDir == Qt::LeftToRight ? Qt::ElideRight : Qt::ElideLeft, w); const QRect rct(left, top, w, sFontHeightCache); QRect outRct; if (ci->softenByBlending()) { qreal oldOpacity = painter->opacity(); painter->setOpacity(0.6); painter->drawText(rct, Qt::AlignTop | Qt::AlignLeft | Qt::TextSingleLine, elidedText, &outRct); painter->setOpacity(oldOpacity); } else { painter->drawText(rct, Qt::AlignTop | Qt::AlignLeft | Qt::TextSingleLine, elidedText, &outRct); } if (layoutDir == Qt::LeftToRight) { left += outRct.width() + gHorizontalItemSpacing; } else { right -= outRct.width() + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_left_aligned_elided_text(const QString &text, Theme::ContentItem *ci, int &left, int top, int &right, QRect &outRect, Qt::LayoutDirection layoutDir, const QFont &font) { Q_UNUSED(font); const QFontMetrics &fontMetrics = cachedFontMetrics(ci); const int w = right - left; const QString elidedText = fontMetrics.elidedText(text, layoutDir == Qt::LeftToRight ? Qt::ElideRight : Qt::ElideLeft, w); const QRect rct(left, top, w, sFontHeightCache); const Qt::AlignmentFlag af = layoutDir == Qt::LeftToRight ? Qt::AlignLeft : Qt::AlignRight; outRect = fontMetrics.boundingRect(rct, Qt::AlignTop | af | Qt::TextSingleLine, elidedText); if (layoutDir == Qt::LeftToRight) { left += outRect.width() + gHorizontalItemSpacing; } else { right -= outRect.width() + gHorizontalItemSpacing; } } static inline const QPixmap *get_read_state_icon(const Theme *theme, Item *item) { if (item->status().isQueued()) { return theme->pixmap(Theme::IconQueued); } else if (item->status().isSent()) { return theme->pixmap(Theme::IconSent); } else if (item->status().isRead()) { return theme->pixmap(Theme::IconRead); } else if (!item->status().isRead()) { return theme->pixmap(Theme::IconUnread); } else if (item->status().isDeleted()) { return theme->pixmap(Theme::IconDeleted); } // Uhm... should never happen.. but fallback to "read"... return theme->pixmap(Theme::IconRead); } static inline const QPixmap *get_combined_read_replied_state_icon(const Theme *theme, MessageItem *messageItem) { if (messageItem->status().isReplied()) { if (messageItem->status().isForwarded()) { return theme->pixmap(Theme::IconRepliedAndForwarded); } return theme->pixmap(Theme::IconReplied); } if (messageItem->status().isForwarded()) { return theme->pixmap(Theme::IconForwarded); } return get_read_state_icon(theme, messageItem); } static inline const QPixmap *get_encryption_state_icon(const Theme *theme, MessageItem *messageItem, bool *treatAsEnabled) { switch (messageItem->encryptionState()) { case MessageItem::FullyEncrypted: *treatAsEnabled = true; return theme->pixmap(Theme::IconFullyEncrypted); case MessageItem::PartiallyEncrypted: *treatAsEnabled = true; return theme->pixmap(Theme::IconPartiallyEncrypted); case MessageItem::EncryptionStateUnknown: *treatAsEnabled = false; return theme->pixmap(Theme::IconUndefinedEncrypted); case MessageItem::NotEncrypted: *treatAsEnabled = false; return theme->pixmap(Theme::IconNotEncrypted); default: // should never happen Q_ASSERT(false); break; } *treatAsEnabled = false; return theme->pixmap(Theme::IconUndefinedEncrypted); } static inline const QPixmap *get_signature_state_icon(const Theme *theme, MessageItem *messageItem, bool *treatAsEnabled) { switch (messageItem->signatureState()) { case MessageItem::FullySigned: *treatAsEnabled = true; return theme->pixmap(Theme::IconFullySigned); case MessageItem::PartiallySigned: *treatAsEnabled = true; return theme->pixmap(Theme::IconPartiallySigned); case MessageItem::SignatureStateUnknown: *treatAsEnabled = false; return theme->pixmap(Theme::IconUndefinedSigned); case MessageItem::NotSigned: *treatAsEnabled = false; return theme->pixmap(Theme::IconNotSigned); default: // should never happen Q_ASSERT(false); break; } *treatAsEnabled = false; return theme->pixmap(Theme::IconUndefinedSigned); } static inline const QPixmap *get_replied_state_icon(const Theme *theme, MessageItem *messageItem) { if (messageItem->status().isReplied()) { if (messageItem->status().isForwarded()) { return theme->pixmap(Theme::IconRepliedAndForwarded); } return theme->pixmap(Theme::IconReplied); } if (messageItem->status().isForwarded()) { return theme->pixmap(Theme::IconForwarded); } return nullptr; } static inline const QPixmap *get_spam_ham_state_icon(const Theme *theme, MessageItem *messageItem) { if (messageItem->status().isSpam()) { return theme->pixmap(Theme::IconSpam); } else if (messageItem->status().isHam()) { return theme->pixmap(Theme::IconHam); } return nullptr; } static inline const QPixmap *get_watched_ignored_state_icon(const Theme *theme, MessageItem *messageItem) { if (messageItem->status().isIgnored()) { return theme->pixmap(Theme::IconIgnored); } else if (messageItem->status().isWatched()) { return theme->pixmap(Theme::IconWatched); } return nullptr; } static inline void paint_vertical_line(QPainter *painter, int &left, int top, int &right, int bottom, bool alignOnRight) { if (alignOnRight) { right -= 1; if (right < 0) { return; } painter->drawLine(right, top, right, bottom); right -= 2; right -= gHorizontalItemSpacing; } else { left += 1; if (left > right) { return; } painter->drawLine(left, top, left, bottom); left += 2 + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_vertical_line(int &left, int top, int &right, int bottom, QRect &outRect, bool alignOnRight) { if (alignOnRight) { right -= 3; outRect = QRect(right, top, 3, bottom - top); right -= gHorizontalItemSpacing; } else { outRect = QRect(left, top, 3, bottom - top); left += 3 + gHorizontalItemSpacing; } } static inline void paint_horizontal_spacer(int &left, int, int &right, int, bool alignOnRight) { if (alignOnRight) { right -= 3 + gHorizontalItemSpacing; } else { left += 3 + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_horizontal_spacer(int &left, int top, int &right, int bottom, QRect &outRect, bool alignOnRight) { if (alignOnRight) { right -= 3; outRect = QRect(right, top, 3, bottom - top); right -= gHorizontalItemSpacing; } else { outRect = QRect(left, top, 3, bottom - top); left += 3 + gHorizontalItemSpacing; } } static inline void paint_permanent_icon(const QPixmap *pix, Theme::ContentItem *, QPainter *painter, int &left, int top, int &right, bool alignOnRight, int iconSize) { if (alignOnRight) { right -= iconSize; // this icon is always present if (right < 0) { return; } painter->drawPixmap(right, top, iconSize, iconSize, *pix); right -= gHorizontalItemSpacing; } else { if (left > (right - iconSize)) { return; } painter->drawPixmap(left, top, iconSize, iconSize, *pix); left += iconSize + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_permanent_icon(Theme::ContentItem *, int &left, int top, int &right, QRect &outRect, bool alignOnRight, int iconSize) { if (alignOnRight) { right -= iconSize; // this icon is always present outRect = QRect(right, top, iconSize, iconSize); right -= gHorizontalItemSpacing; } else { outRect = QRect(left, top, iconSize, iconSize); left += iconSize + gHorizontalItemSpacing; } } static inline void paint_boolean_state_icon(bool enabled, const QPixmap *pix, Theme::ContentItem *ci, QPainter *painter, int &left, int top, int &right, bool alignOnRight, int iconSize) { if (enabled) { paint_permanent_icon(pix, ci, painter, left, top, right, alignOnRight, iconSize); return; } // off -> icon disabled if (ci->hideWhenDisabled()) { return; // doesn't even take space } if (ci->softenByBlendingWhenDisabled()) { // still paint, but very soft qreal oldOpacity = painter->opacity(); painter->setOpacity(0.1); paint_permanent_icon(pix, ci, painter, left, top, right, alignOnRight, iconSize); painter->setOpacity(oldOpacity); return; } // just takes space if (alignOnRight) { right -= iconSize + gHorizontalItemSpacing; } else { left += iconSize + gHorizontalItemSpacing; } } static inline void compute_bounding_rect_for_boolean_state_icon(bool enabled, Theme::ContentItem *ci, int &left, int top, int &right, QRect &outRect, bool alignOnRight, int iconSize) { if ((!enabled) && ci->hideWhenDisabled()) { outRect = QRect(); return; // doesn't even take space } compute_bounding_rect_for_permanent_icon(ci, left, top, right, outRect, alignOnRight, iconSize); } static inline void paint_tag_list(const QList< MessageItem::Tag * > &tagList, QPainter *painter, int &left, int top, int &right, bool alignOnRight, int iconSize) { if (alignOnRight) { for (const MessageItem::Tag *tag : tagList) { right -= iconSize; // this icon is always present if (right < 0) { return; } painter->drawPixmap(right, top, iconSize, iconSize, tag->pixmap()); right -= gHorizontalItemSpacing; } } else { for (const MessageItem::Tag *tag : tagList) { if (left > right - iconSize) { return; } painter->drawPixmap(left, top, iconSize, iconSize, tag->pixmap()); left += iconSize + gHorizontalItemSpacing; } } } static inline void compute_bounding_rect_for_tag_list(const QList< MessageItem::Tag * > &tagList, int &left, int top, int &right, QRect &outRect, bool alignOnRight, int iconSize) { int width = tagList.count() * (iconSize + gHorizontalItemSpacing); if (alignOnRight) { right -= width; outRect = QRect(right, top, width, iconSize); } else { outRect = QRect(left, top, width, iconSize); left += width; } } static inline void compute_size_hint_for_item(Theme::ContentItem *ci, int &maxh, int &totalw, int iconSize, const Item *item) { Q_UNUSED(item); if (ci->displaysText()) { if (sFontHeightCache > maxh) { maxh = sFontHeightCache; } totalw += ci->displaysLongText() ? 128 : 64; return; } if (ci->isIcon()) { totalw += iconSize + gHorizontalItemSpacing; if (maxh < iconSize) { maxh = iconSize; } return; } if (ci->isSpacer()) { if (18 > maxh) { maxh = 18; } totalw += 3 + gHorizontalItemSpacing; return; } // should never be reached if (18 > maxh) { maxh = 18; } totalw += gHorizontalItemSpacing; } static inline QSize compute_size_hint_for_row(const Theme::Row *r, int iconSize, const Item *item) { int maxh = 8; // at least 8 pixels for a pixmap int totalw = 0; // right aligned stuff first auto items = r->rightItems(); for (const auto it : qAsConst(items)) { compute_size_hint_for_item(const_cast(it), maxh, totalw, iconSize, item); } // then left aligned stuff items = r->leftItems(); for (const auto it : qAsConst(items)) { compute_size_hint_for_item(const_cast(it), maxh, totalw, iconSize, item); } return QSize(totalw, maxh); } void ThemeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (!index.isValid()) { return; // bleah } Item *item = itemFromIndex(index); if (!item) { return; // hm... } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); opt.text.clear(); // draw no text for me, please.. I'll do it in a while // Set background color of control if necessary if (item->type() == Item::Message) { MessageItem *msgItem = static_cast< MessageItem * >(item); if (msgItem->backgroundColor().isValid()) { opt.backgroundBrush = QBrush(msgItem->backgroundColor()); } } QStyle *style = mItemView->style(); style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, mItemView); if (!mTheme) { return; // hm hm... } const Theme::Column *skcolumn = mTheme->column(index.column()); if (!skcolumn) { return; // bleah } const QList *rows; MessageItem *messageItem = nullptr; GroupHeaderItem *groupHeaderItem = nullptr; int top = opt.rect.top(); int right = opt.rect.left() + opt.rect.width(); // don't use opt.rect.right() since it's screwed int left = opt.rect.left(); // Storing the changed members one by one is faster than saving the painter state QFont oldFont = painter->font(); QPen oldPen = painter->pen(); qreal oldOpacity = painter->opacity(); QPen defaultPen; bool usingNonDefaultTextColor = false; switch (item->type()) { case Item::Message: rows = &(skcolumn->messageRows()); messageItem = static_cast< MessageItem * >(item); if ( (!(opt.state & QStyle::State_Enabled)) || messageItem->aboutToBeRemoved() || (!messageItem->isValid()) ) { painter->setOpacity(0.5); defaultPen = QPen(opt.palette.brush(QPalette::Disabled, QPalette::Text), 0); } else { QPalette::ColorGroup cg; if (opt.state & QStyle::State_Active) { cg = QPalette::Normal; } else { cg = QPalette::Inactive; } if (opt.state & QStyle::State_Selected) { defaultPen = QPen(opt.palette.brush(cg, QPalette::HighlightedText), 0); } else { if (messageItem->textColor().isValid()) { usingNonDefaultTextColor = true; defaultPen = QPen(messageItem->textColor(), 0); } else { defaultPen = QPen(opt.palette.brush(cg, QPalette::Text), 0); } } } top += gMessageVerticalMargin; right -= gMessageHorizontalMargin; left += gMessageHorizontalMargin; break; case Item::GroupHeader: { rows = &(skcolumn->groupHeaderRows()); groupHeaderItem = static_cast< GroupHeaderItem * >(item); QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active)) { cg = QPalette::Inactive; } QPalette::ColorRole cr; top += gGroupHeaderOuterVerticalMargin; right -= gGroupHeaderOuterHorizontalMargin; left += gGroupHeaderOuterHorizontalMargin; switch (mTheme->groupHeaderBackgroundMode()) { case Theme::Transparent: cr = (opt.state & QStyle::State_Selected) ? QPalette::HighlightedText : QPalette::Text; defaultPen = QPen(opt.palette.brush(cg, cr), 0); break; case Theme::AutoColor: case Theme::CustomColor: switch (mTheme->groupHeaderBackgroundStyle()) { case Theme::PlainRect: painter->fillRect( QRect(left, top, right - left, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(mGroupHeaderBackgroundColor) ); break; case Theme::PlainJoinedRect: { int rleft = (opt.viewItemPosition == QStyleOptionViewItem::Beginning) || (opt.viewItemPosition == QStyleOptionViewItem::OnlyOne) ? left : opt.rect.left(); int rright = (opt.viewItemPosition == QStyleOptionViewItem::End) || (opt.viewItemPosition == QStyleOptionViewItem::OnlyOne) ? right : opt.rect.left() + opt.rect.width(); painter->fillRect( QRect(rleft, top, rright - rleft, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(mGroupHeaderBackgroundColor) ); break; } case Theme::RoundedJoinedRect: if (opt.viewItemPosition == QStyleOptionViewItem::Middle) { painter->fillRect( QRect(opt.rect.left(), top, opt.rect.width(), opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(mGroupHeaderBackgroundColor) ); break; // don't fall through } if (opt.viewItemPosition == QStyleOptionViewItem::Beginning) { painter->fillRect( QRect(opt.rect.left() + opt.rect.width() - 10, top, 10, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(mGroupHeaderBackgroundColor) ); } else if (opt.viewItemPosition == QStyleOptionViewItem::End) { painter->fillRect( QRect(opt.rect.left(), top, 10, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(mGroupHeaderBackgroundColor) ); } // fall through anyway Q_FALLTHROUGH(); case Theme::RoundedRect: { painter->setPen(Qt::NoPen); bool hadAntialiasing = painter->renderHints() & QPainter::Antialiasing; if (!hadAntialiasing) { painter->setRenderHint(QPainter::Antialiasing, true); } painter->setBrush(QBrush(mGroupHeaderBackgroundColor)); painter->setBackgroundMode(Qt::OpaqueMode); int w = right - left; if (w > 0) { painter->drawRoundedRect( QRect(left, top, w, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), 4.0, 4.0 ); } if (!hadAntialiasing) { painter->setRenderHint(QPainter::Antialiasing, false); } painter->setBackgroundMode(Qt::TransparentMode); break; } case Theme::GradientJoinedRect: { // FIXME: Could cache this brush QLinearGradient gradient(0, top, 0, top + opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)); gradient.setColorAt(0.0, KColorScheme::shade(mGroupHeaderBackgroundColor, KColorScheme::LightShade, 0.3)); gradient.setColorAt(1.0, mGroupHeaderBackgroundColor); if (opt.viewItemPosition == QStyleOptionViewItem::Middle) { painter->fillRect( QRect(opt.rect.left(), top, opt.rect.width(), opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(gradient) ); break; // don't fall through } if (opt.viewItemPosition == QStyleOptionViewItem::Beginning) { painter->fillRect( QRect(opt.rect.left() + opt.rect.width() - 10, top, 10, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(gradient) ); } else if (opt.viewItemPosition == QStyleOptionViewItem::End) { painter->fillRect( QRect(opt.rect.left(), top, 10, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), QBrush(gradient) ); } // fall through anyway Q_FALLTHROUGH(); } case Theme::GradientRect: { // FIXME: Could cache this brush QLinearGradient gradient(0, top, 0, top + opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)); gradient.setColorAt(0.0, KColorScheme::shade(mGroupHeaderBackgroundColor, KColorScheme::LightShade, 0.3)); gradient.setColorAt(1.0, mGroupHeaderBackgroundColor); painter->setPen(Qt::NoPen); bool hadAntialiasing = painter->renderHints() & QPainter::Antialiasing; if (!hadAntialiasing) { painter->setRenderHint(QPainter::Antialiasing, true); } painter->setBrush(QBrush(gradient)); painter->setBackgroundMode(Qt::OpaqueMode); int w = right - left; if (w > 0) { painter->drawRoundedRect( QRect(left, top, w, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)), 4.0, 4.0 ); } if (!hadAntialiasing) { painter->setRenderHint(QPainter::Antialiasing, false); } painter->setBackgroundMode(Qt::TransparentMode); break; } case Theme::StyledRect: // oxygen, for instance, has a nice graphics for selected items opt.rect = QRect(left, top, right - left, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)); opt.state |= QStyle::State_Selected; opt.viewItemPosition = QStyleOptionViewItem::OnlyOne; opt.palette.setColor(cg, QPalette::Highlight, mGroupHeaderBackgroundColor); style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, mItemView); break; case Theme::StyledJoinedRect: { int rleft = (opt.viewItemPosition == QStyleOptionViewItem::Beginning) || (opt.viewItemPosition == QStyleOptionViewItem::OnlyOne) ? left : opt.rect.left(); int rright = (opt.viewItemPosition == QStyleOptionViewItem::End) || (opt.viewItemPosition == QStyleOptionViewItem::OnlyOne) ? right : opt.rect.left() + opt.rect.width(); opt.rect = QRect(rleft, top, rright - rleft, opt.rect.height() - (gGroupHeaderInnerVerticalMargin * 2)); opt.state |= QStyle::State_Selected; opt.palette.setColor(cg, QPalette::Highlight, mGroupHeaderBackgroundColor); style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, mItemView); break; } } defaultPen = QPen(opt.palette.brush(cg, QPalette::Text), 0); break; } top += gGroupHeaderInnerVerticalMargin; right -= gGroupHeaderInnerHorizontalMargin; left += gGroupHeaderInnerHorizontalMargin; break; } default: Q_ASSERT(false); return; // bug } Qt::LayoutDirection layoutDir = mItemView->layoutDirection(); for (const auto row : qAsConst(*rows)) { QSize rowSizeHint = compute_size_hint_for_row(row, mTheme->iconSize(), item); int bottom = top + rowSizeHint.height(); // paint right aligned stuff first int r = right; int l = left; const auto rightItems = row->rightItems(); for (const auto itemit : rightItems) { auto ci = const_cast(itemit); if (ci->canUseCustomColor()) { if (ci->useCustomColor() && (!(opt.state & QStyle::State_Selected))) { if (usingNonDefaultTextColor) { // merge the colors QColor nonDefault = defaultPen.color(); QColor custom = ci->customColor(); QColor merged( (nonDefault.red() + custom.red()) >> 1, (nonDefault.green() + custom.green()) >> 1, (nonDefault.blue() + custom.blue()) >> 1 ); painter->setPen(QPen(merged)); } else { painter->setPen(QPen(ci->customColor())); } } else { painter->setPen(defaultPen); } } // otherwise setting a pen is useless at this time const QFont &font = cachedFont(ci, item); switch (ci->type()) { case Theme::ContentItem::Subject: paint_right_aligned_elided_text(item->subject(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::SenderOrReceiver: paint_right_aligned_elided_text(item->displaySenderOrReceiver(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Receiver: paint_right_aligned_elided_text(item->displayReceiver(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Sender: paint_right_aligned_elided_text(item->displaySender(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Date: paint_right_aligned_elided_text(item->formattedDate(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::MostRecentDate: paint_right_aligned_elided_text(item->formattedMaxDate(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Size: paint_right_aligned_elided_text(item->formattedSize(), ci, painter, l, top, r, layoutDir, font); break; + case Theme::ContentItem::Folder: + paint_right_aligned_elided_text(item->folder(), ci, painter, l, top, r, layoutDir, font); + break; case Theme::ContentItem::GroupHeaderLabel: if (groupHeaderItem) { paint_right_aligned_elided_text(groupHeaderItem->label(), ci, painter, l, top, r, layoutDir, font); } break; case Theme::ContentItem::ReadStateIcon: paint_permanent_icon(get_read_state_icon(mTheme, item), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::CombinedReadRepliedStateIcon: if (messageItem) { paint_permanent_icon(get_combined_read_replied_state_icon(mTheme, messageItem), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ExpandedStateIcon: { const QPixmap *pix = item->childItemCount() > 0 ? mTheme->pixmap((option.state & QStyle::State_Open) ? Theme::IconShowLess : Theme::IconShowMore) : nullptr; paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconShowMore), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); break; } case Theme::ContentItem::RepliedStateIcon: if (messageItem) { const QPixmap *pix = get_replied_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconReplied), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::EncryptionStateIcon: if (messageItem) { bool enabled; const QPixmap *pix = get_encryption_state_icon(mTheme, messageItem, &enabled); paint_boolean_state_icon(enabled, pix, ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SignatureStateIcon: if (messageItem) { bool enabled; const QPixmap *pix = get_signature_state_icon(mTheme, messageItem, &enabled); paint_boolean_state_icon(enabled, pix, ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SpamHamStateIcon: if (messageItem) { const QPixmap *pix = get_spam_ham_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconSpam), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::WatchedIgnoredStateIcon: if (messageItem) { const QPixmap *pix = get_watched_ignored_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconWatched), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AttachmentStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().hasAttachment(), mTheme->pixmap(Theme::IconAttachment), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AnnotationIcon: if (messageItem) { paint_boolean_state_icon(messageItem->hasAnnotation(), mTheme->pixmap(Theme::IconAnnotation), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::InvitationIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().hasInvitation(), mTheme->pixmap(Theme::IconInvitation), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ActionItemStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().isToAct(), mTheme->pixmap(Theme::IconActionItem), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ImportantStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().isImportant(), mTheme->pixmap(Theme::IconImportant), ci, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::VerticalLine: paint_vertical_line(painter, l, top, r, bottom, layoutDir == Qt::LeftToRight); break; case Theme::ContentItem::HorizontalSpacer: paint_horizontal_spacer(l, top, r, bottom, layoutDir == Qt::LeftToRight); break; case Theme::ContentItem::TagList: if (messageItem) { const QList< MessageItem::Tag * > tagList = messageItem->tagList(); paint_tag_list(tagList, painter, l, top, r, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; } } // then paint left aligned stuff const auto leftItems = row->leftItems(); for (const auto itemit : leftItems) { auto ci = const_cast(itemit); if (ci->canUseCustomColor()) { if (ci->useCustomColor() && (!(opt.state & QStyle::State_Selected))) { if (usingNonDefaultTextColor) { // merge the colors QColor nonDefault = defaultPen.color(); QColor custom = ci->customColor(); QColor merged( (nonDefault.red() + custom.red()) >> 1, (nonDefault.green() + custom.green()) >> 1, (nonDefault.blue() + custom.blue()) >> 1 ); painter->setPen(QPen(merged)); } else { painter->setPen(QPen(ci->customColor())); } } else { painter->setPen(defaultPen); } } // otherwise setting a pen is useless at this time const QFont &font = cachedFont(ci, item); switch (ci->type()) { case Theme::ContentItem::Subject: paint_left_aligned_elided_text(item->subject(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::SenderOrReceiver: paint_left_aligned_elided_text(item->displaySenderOrReceiver(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Receiver: paint_left_aligned_elided_text(item->displayReceiver(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Sender: paint_left_aligned_elided_text(item->displaySender(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Date: paint_left_aligned_elided_text(item->formattedDate(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::MostRecentDate: paint_left_aligned_elided_text(item->formattedMaxDate(), ci, painter, l, top, r, layoutDir, font); break; case Theme::ContentItem::Size: paint_left_aligned_elided_text(item->formattedSize(), ci, painter, l, top, r, layoutDir, font); break; + case Theme::ContentItem::Folder: + paint_left_aligned_elided_text(item->folder(), ci, painter, l, top, r, layoutDir, font); + break; case Theme::ContentItem::GroupHeaderLabel: if (groupHeaderItem) { paint_left_aligned_elided_text(groupHeaderItem->label(), ci, painter, l, top, r, layoutDir, font); } break; case Theme::ContentItem::ReadStateIcon: paint_permanent_icon(get_read_state_icon(mTheme, item), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::CombinedReadRepliedStateIcon: if (messageItem) { paint_permanent_icon(get_combined_read_replied_state_icon(mTheme, messageItem), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ExpandedStateIcon: { const QPixmap *pix = item->childItemCount() > 0 ? mTheme->pixmap((option.state & QStyle::State_Open) ? Theme::IconShowLess : Theme::IconShowMore) : nullptr; paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconShowMore), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); break; } case Theme::ContentItem::RepliedStateIcon: if (messageItem) { const QPixmap *pix = get_replied_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconReplied), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::EncryptionStateIcon: if (messageItem) { bool enabled; const QPixmap *pix = get_encryption_state_icon(mTheme, messageItem, &enabled); paint_boolean_state_icon(enabled, pix, ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SignatureStateIcon: if (messageItem) { bool enabled; const QPixmap *pix = get_signature_state_icon(mTheme, messageItem, &enabled); paint_boolean_state_icon(enabled, pix, ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SpamHamStateIcon: if (messageItem) { const QPixmap *pix = get_spam_ham_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconSpam), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::WatchedIgnoredStateIcon: if (messageItem) { const QPixmap *pix = get_watched_ignored_state_icon(mTheme, messageItem); paint_boolean_state_icon(pix != nullptr, pix ? pix : mTheme->pixmap(Theme::IconWatched), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AttachmentStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().hasAttachment(), mTheme->pixmap(Theme::IconAttachment), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AnnotationIcon: if (messageItem) { paint_boolean_state_icon(messageItem->hasAnnotation(), mTheme->pixmap(Theme::IconAnnotation), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::InvitationIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().hasInvitation(), mTheme->pixmap(Theme::IconInvitation), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ActionItemStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().isToAct(), mTheme->pixmap(Theme::IconActionItem), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ImportantStateIcon: if (messageItem) { paint_boolean_state_icon(messageItem->status().isImportant(), mTheme->pixmap(Theme::IconImportant), ci, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::VerticalLine: paint_vertical_line(painter, l, top, r, bottom, layoutDir != Qt::LeftToRight); break; case Theme::ContentItem::HorizontalSpacer: paint_horizontal_spacer(l, top, r, bottom, layoutDir != Qt::LeftToRight); break; case Theme::ContentItem::TagList: if (messageItem) { const QList< MessageItem::Tag * > tagList = messageItem->tagList(); paint_tag_list(tagList, painter, l, top, r, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; } } top = bottom; } painter->setFont(oldFont); painter->setPen(oldPen); painter->setOpacity(oldOpacity); } bool ThemeDelegate::hitTest(const QPoint &viewportPoint, bool exact) { mHitItem = nullptr; mHitColumn = nullptr; mHitRow = nullptr; mHitContentItem = nullptr; if (!mTheme) { return false; // hm hm... } mHitIndex = mItemView->indexAt(viewportPoint); if (!mHitIndex.isValid()) { return false; // bleah } mHitItem = itemFromIndex(mHitIndex); if (!mHitItem) { return false; // hm... } mHitItemRect = mItemView->visualRect(mHitIndex); mHitColumn = mTheme->column(mHitIndex.column()); if (!mHitColumn) { return false; // bleah } const QList< Theme::Row * > *rows; // I'd like to have it as reference, but gcc complains... MessageItem *messageItem = nullptr; GroupHeaderItem *groupHeaderItem = nullptr; int top = mHitItemRect.top(); int right = mHitItemRect.right(); int left = mHitItemRect.left(); mHitRow = nullptr; mHitRowIndex = -1; mHitContentItem = nullptr; switch (mHitItem->type()) { case Item::Message: mHitRowIsMessageRow = true; rows = &(mHitColumn->messageRows()); messageItem = static_cast< MessageItem * >(mHitItem); // FIXME: paint eventual background here top += gMessageVerticalMargin; right -= gMessageHorizontalMargin; left += gMessageHorizontalMargin; break; case Item::GroupHeader: mHitRowIsMessageRow = false; rows = &(mHitColumn->groupHeaderRows()); groupHeaderItem = static_cast< GroupHeaderItem * >(mHitItem); top += gGroupHeaderOuterVerticalMargin + gGroupHeaderInnerVerticalMargin; right -= gGroupHeaderOuterHorizontalMargin + gGroupHeaderInnerHorizontalMargin; left += gGroupHeaderOuterHorizontalMargin + gGroupHeaderInnerHorizontalMargin; break; default: return false; // bug break; } int rowIdx = 0; int bestInexactDistance = 0xffffff; bool bestInexactItemRight = false; QRect bestInexactRect; const Theme::ContentItem *bestInexactContentItem = nullptr; Qt::LayoutDirection layoutDir = mItemView->layoutDirection(); for (const auto row : qAsConst(*rows)) { QSize rowSizeHint = compute_size_hint_for_row(row, mTheme->iconSize(), mHitItem); if ((viewportPoint.y() < top) && (rowIdx > 0)) { break; // not this row (tough we should have already found it... probably clicked upper margin) } int bottom = top + rowSizeHint.height(); if (viewportPoint.y() > bottom) { top += rowSizeHint.height(); rowIdx++; continue; // not this row } bestInexactItemRight = false; bestInexactDistance = 0xffffff; bestInexactContentItem = nullptr; // this row! mHitRow = row; mHitRowIndex = rowIdx; mHitRowRect = QRect(left, top, right - left, bottom - top); // check right aligned stuff first mHitContentItemRight = true; int r = right; int l = left; const auto rightItems = mHitRow->rightItems(); for (const auto itemit : rightItems) { auto ci = const_cast(itemit); mHitContentItemRect = QRect(); const QFont &font = cachedFont(ci, mHitItem); switch (ci->type()) { case Theme::ContentItem::Subject: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->subject(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::SenderOrReceiver: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->displaySenderOrReceiver(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Receiver: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->displayReceiver(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Sender: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->displaySender(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Date: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->formattedDate(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::MostRecentDate: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->formattedMaxDate(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Size: compute_bounding_rect_for_right_aligned_elided_text(mHitItem->formattedSize(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; + case Theme::ContentItem::Folder: + compute_bounding_rect_for_right_aligned_elided_text(mHitItem->folder(), ci, l, top, r, mHitContentItemRect, layoutDir, font); + break; case Theme::ContentItem::GroupHeaderLabel: if (groupHeaderItem) { compute_bounding_rect_for_right_aligned_elided_text(groupHeaderItem->label(), ci, l, top, r, mHitContentItemRect, layoutDir, font); } break; case Theme::ContentItem::ReadStateIcon: compute_bounding_rect_for_permanent_icon(ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::CombinedReadRepliedStateIcon: compute_bounding_rect_for_permanent_icon(ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::ExpandedStateIcon: compute_bounding_rect_for_boolean_state_icon(mHitItem->childItemCount() > 0, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::RepliedStateIcon: if (messageItem) { const QPixmap *pix = get_replied_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::EncryptionStateIcon: if (messageItem) { bool enabled; get_encryption_state_icon(mTheme, messageItem, &enabled); compute_bounding_rect_for_boolean_state_icon(enabled, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SignatureStateIcon: if (messageItem) { bool enabled; get_signature_state_icon(mTheme, messageItem, &enabled); compute_bounding_rect_for_boolean_state_icon(enabled, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SpamHamStateIcon: if (messageItem) { const QPixmap *pix = get_spam_ham_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::WatchedIgnoredStateIcon: if (messageItem) { const QPixmap *pix = get_watched_ignored_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AttachmentStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().hasAttachment(), ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AnnotationIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->hasAnnotation(), ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::InvitationIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().hasInvitation(), ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ActionItemStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().isToAct(), ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ImportantStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().isImportant(), ci, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::VerticalLine: compute_bounding_rect_for_vertical_line(l, top, r, bottom, mHitContentItemRect, layoutDir == Qt::LeftToRight); break; case Theme::ContentItem::HorizontalSpacer: compute_bounding_rect_for_horizontal_spacer(l, top, r, bottom, mHitContentItemRect, layoutDir == Qt::LeftToRight); break; case Theme::ContentItem::TagList: if (messageItem) { const QList< MessageItem::Tag * > tagList = messageItem->tagList(); compute_bounding_rect_for_tag_list(tagList, l, top, r, mHitContentItemRect, layoutDir == Qt::LeftToRight, mTheme->iconSize()); } break; } if (mHitContentItemRect.isValid()) { if (mHitContentItemRect.contains(viewportPoint)) { // caught! mHitContentItem = ci; return true; } if (!exact) { QRect inexactRect(mHitContentItemRect.left(), mHitRowRect.top(), mHitContentItemRect.width(), mHitRowRect.height()); if (inexactRect.contains(viewportPoint)) { mHitContentItem = ci; return true; } int inexactDistance = viewportPoint.x() > inexactRect.right() ? viewportPoint.x() - inexactRect.right() : inexactRect.left() - viewportPoint.x(); if (inexactDistance < bestInexactDistance) { bestInexactDistance = inexactDistance; bestInexactRect = mHitContentItemRect; bestInexactItemRight = true; bestInexactContentItem = ci; } } } } // then check left aligned stuff mHitContentItemRight = false; const auto leftItems = mHitRow->leftItems(); for (const auto itemit : leftItems) { auto ci = const_cast(itemit); mHitContentItemRect = QRect(); const QFont &font = cachedFont(ci, mHitItem); switch (ci->type()) { case Theme::ContentItem::Subject: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->subject(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::SenderOrReceiver: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->displaySenderOrReceiver(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Receiver: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->displayReceiver(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Sender: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->displaySender(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Date: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->formattedDate(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::MostRecentDate: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->formattedMaxDate(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; case Theme::ContentItem::Size: compute_bounding_rect_for_left_aligned_elided_text(mHitItem->formattedSize(), ci, l, top, r, mHitContentItemRect, layoutDir, font); break; + case Theme::ContentItem::Folder: + compute_bounding_rect_for_left_aligned_elided_text(mHitItem->folder(), ci, l, top, r, mHitContentItemRect, layoutDir, font); + break; case Theme::ContentItem::GroupHeaderLabel: if (groupHeaderItem) { compute_bounding_rect_for_left_aligned_elided_text(groupHeaderItem->label(), ci, l, top, r, mHitContentItemRect, layoutDir, font); } break; case Theme::ContentItem::ReadStateIcon: compute_bounding_rect_for_permanent_icon(ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::CombinedReadRepliedStateIcon: compute_bounding_rect_for_permanent_icon(ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::ExpandedStateIcon: compute_bounding_rect_for_boolean_state_icon(mHitItem->childItemCount() > 0, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); break; case Theme::ContentItem::RepliedStateIcon: if (messageItem) { const QPixmap *pix = get_replied_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::EncryptionStateIcon: if (messageItem) { bool enabled; get_encryption_state_icon(mTheme, messageItem, &enabled); compute_bounding_rect_for_boolean_state_icon(enabled, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SignatureStateIcon: if (messageItem) { bool enabled; get_signature_state_icon(mTheme, messageItem, &enabled); compute_bounding_rect_for_boolean_state_icon(enabled, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::SpamHamStateIcon: if (messageItem) { const QPixmap *pix = get_spam_ham_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::WatchedIgnoredStateIcon: if (messageItem) { const QPixmap *pix = get_watched_ignored_state_icon(mTheme, messageItem); compute_bounding_rect_for_boolean_state_icon(pix != nullptr, ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AttachmentStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().hasAttachment(), ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::AnnotationIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->hasAnnotation(), ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::InvitationIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().hasInvitation(), ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ActionItemStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().isToAct(), ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::ImportantStateIcon: if (messageItem) { compute_bounding_rect_for_boolean_state_icon(messageItem->status().isImportant(), ci, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; case Theme::ContentItem::VerticalLine: compute_bounding_rect_for_vertical_line(l, top, r, bottom, mHitContentItemRect, layoutDir != Qt::LeftToRight); break; case Theme::ContentItem::HorizontalSpacer: compute_bounding_rect_for_horizontal_spacer(l, top, r, bottom, mHitContentItemRect, layoutDir != Qt::LeftToRight); break; case Theme::ContentItem::TagList: if (messageItem) { const QList< MessageItem::Tag * > tagList = messageItem->tagList(); compute_bounding_rect_for_tag_list(tagList, l, top, r, mHitContentItemRect, layoutDir != Qt::LeftToRight, mTheme->iconSize()); } break; } if (mHitContentItemRect.isValid()) { if (mHitContentItemRect.contains(viewportPoint)) { // caught! mHitContentItem = ci; return true; } if (!exact) { QRect inexactRect(mHitContentItemRect.left(), mHitRowRect.top(), mHitContentItemRect.width(), mHitRowRect.height()); if (inexactRect.contains(viewportPoint)) { mHitContentItem = ci; return true; } int inexactDistance = viewportPoint.x() > inexactRect.right() ? viewportPoint.x() - inexactRect.right() : inexactRect.left() - viewportPoint.x(); if (inexactDistance < bestInexactDistance) { bestInexactDistance = inexactDistance; bestInexactRect = mHitContentItemRect; bestInexactItemRight = false; bestInexactContentItem = ci; } } } } top += rowSizeHint.height(); rowIdx++; } mHitContentItem = bestInexactContentItem; mHitContentItemRight = bestInexactItemRight; mHitContentItemRect = bestInexactRect; return true; } const QModelIndex &ThemeDelegate::hitIndex() const { return mHitIndex; } Item *ThemeDelegate::hitItem() const { return mHitItem; } QRect ThemeDelegate::hitItemRect() const { return mHitItemRect; } const Theme::Column *ThemeDelegate::hitColumn() const { return mHitColumn; } int ThemeDelegate::hitColumnIndex() const { return mHitIndex.column(); } const Theme::Row *ThemeDelegate::hitRow() const { return mHitRow; } int ThemeDelegate::hitRowIndex() const { return mHitRowIndex; } QRect ThemeDelegate::hitRowRect() const { return mHitRowRect; } bool ThemeDelegate::hitRowIsMessageRow() const { return mHitRowIsMessageRow; } const Theme::ContentItem *ThemeDelegate::hitContentItem() const { return mHitContentItem; } bool ThemeDelegate::hitContentItemRight() const { return mHitContentItemRight; } QRect ThemeDelegate::hitContentItemRect() const { return mHitContentItemRect; } QSize ThemeDelegate::sizeHintForItemTypeAndColumn(Item::Type type, int column, const Item *item) const { if (!mTheme) { return QSize(16, 16); // bleah } const Theme::Column *skcolumn = mTheme->column(column); if (!skcolumn) { return QSize(16, 16); // bleah } const QList< Theme::Row * > *rows; // I'd like to have it as reference, but gcc complains... // The sizeHint() is layout direction independent. int marginw; int marginh; switch (type) { case Item::Message: rows = &(skcolumn->messageRows()); marginh = gMessageVerticalMargin << 1; marginw = gMessageHorizontalMargin << 1; break; case Item::GroupHeader: rows = &(skcolumn->groupHeaderRows()); marginh = (gGroupHeaderOuterVerticalMargin + gGroupHeaderInnerVerticalMargin) << 1; marginw = (gGroupHeaderOuterVerticalMargin + gGroupHeaderInnerVerticalMargin) << 1; break; default: return QSize(16, 16); // bug break; } int totalh = 0; int maxw = 0; for (QList< Theme::Row * >::ConstIterator rowit = rows->constBegin(), endRowIt = rows->constEnd(); rowit != endRowIt; ++rowit) { const QSize sh = compute_size_hint_for_row((*rowit), mTheme->iconSize(), item); totalh += sh.height(); if (sh.width() > maxw) { maxw = sh.width(); } } return QSize(maxw + marginw, totalh + marginh); } QSize ThemeDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &index) const { if (!mTheme || !index.isValid()) { return QSize(16, 16); // hm hm... } Item *item = itemFromIndex(index); if (!item) { return QSize(16, 16); // hm... } const Item::Type type = item->type(); if (type == Item::Message) { if (!mCachedMessageItemSizeHint.isValid()) { mCachedMessageItemSizeHint = sizeHintForItemTypeAndColumn(Item::Message, index.column(), item); } return mCachedMessageItemSizeHint; } else if (type == Item::GroupHeader) { if (!mCachedGroupHeaderItemSizeHint.isValid()) { mCachedGroupHeaderItemSizeHint = sizeHintForItemTypeAndColumn(Item::GroupHeader, index.column(), item); } return mCachedGroupHeaderItemSizeHint; } else { Q_ASSERT(false); return QSize(); } } // Store the new fonts when the generalFont changes and flush sizeHint cache void ThemeDelegate::generalFontChanged() { mCachedMessageItemSizeHint = QSize(); mCachedGroupHeaderItemSizeHint = QSize(); QFont font; if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) { font = QFontDatabase::systemFont(QFontDatabase::GeneralFont); } else { font = MessageListSettings::self()->messageListFont(); } sFontCache[Normal] = font; sFontMetricsCache[Normal] = QFontMetrics(font); font.setBold(true); sFontCache[Bold] = font; sFontMetricsCache[Bold] = QFontMetrics(font); font.setBold(false); font.setItalic(true); sFontCache[Italic] = font; sFontMetricsCache[Italic] = QFontMetrics(font); font.setBold(true); font.setItalic(true); sFontCache[BoldItalic] = font; sFontMetricsCache[BoldItalic] = QFontMetrics(font); sFontHeightCache = sFontMetricsCache[Normal].height(); } const Theme *ThemeDelegate::theme() const { return mTheme; } diff --git a/messagelist/src/core/view.cpp b/messagelist/src/core/view.cpp index fdf6f031..37323bcd 100644 --- a/messagelist/src/core/view.cpp +++ b/messagelist/src/core/view.cpp @@ -1,2753 +1,2755 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "core/view.h" #include "core/aggregation.h" #include "core/delegate.h" #include "core/groupheaderitem.h" #include "core/item.h" #include "core/manager.h" #include "core/messageitem.h" #include "core/model.h" #include "core/theme.h" #include "messagelistsettings.h" #include "core/storagemodelbase.h" #include "core/widgetbase.h" #include "messagelistutil.h" #include "messagelistutil_p.h" #include "MessageCore/StringUtil" #include // kdepimlibs #include #include #include #include #include #include #include #include #include #include #include #include #include "messagelist_debug.h" using namespace MessageList::Core; class Q_DECL_HIDDEN View::Private { public: Private(View *owner, Widget *parent) : q(owner) , mWidget(parent) , mDelegate(new Delegate(owner)) { } void expandFullThread(const QModelIndex &index); void generalPaletteChanged(); QColor mTextColor; View *const q; Widget *mWidget = nullptr; Model *mModel = nullptr; Delegate *mDelegate = nullptr; const Aggregation *mAggregation = nullptr; ///< The Aggregation we're using now, shallow pointer Theme *mTheme = nullptr; ///< The Theme we're using now, shallow pointer bool mNeedToApplyThemeColumns = false; ///< Flag signaling a pending application of theme columns Item *mLastCurrentItem = nullptr; QPoint mMousePressPosition; bool mSaveThemeColumnStateOnSectionResize = true; ///< This is used to filter out programmatic column resizes in slotSectionResized(). QTimer *mSaveThemeColumnStateTimer = nullptr; ///< Used to trigger a delayed "save theme state" QTimer *mApplyThemeColumnsTimer = nullptr; ///< Used to trigger a delayed "apply theme columns" int mLastViewportWidth = -1; bool mIgnoreUpdateGeometries = false; ///< Shall we ignore the "update geometries" calls ? }; View::View(Widget *pParent) : QTreeView(pParent) , d(new Private(this, pParent)) { d->mSaveThemeColumnStateTimer = new QTimer(); connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState); d->mApplyThemeColumnsTimer = new QTimer(); connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns); setItemDelegate(d->mDelegate); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setAlternatingRowColors(true); setAllColumnsShowFocus(true); setSelectionMode(QAbstractItemView::ExtendedSelection); viewport()->setAcceptDrops(true); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested); connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized); header()->setSectionsClickable(true); header()->setSectionResizeMode(QHeaderView::Interactive); header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value d->mModel = new Model(this); setModel(d->mModel); connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); // as in KDE3, when a root-item of a message thread is expanded, expand all children connect(this, &View::expanded, this, [this](const QModelIndex &index) { d->expandFullThread(index); }); } View::~View() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } delete d->mSaveThemeColumnStateTimer; if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } delete d->mApplyThemeColumnsTimer; // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process d->mApplyThemeColumnsTimer = nullptr; d->mTheme = nullptr; d->mAggregation = nullptr; delete d; d = nullptr; } Model *View::model() const { return d->mModel; } Delegate *View::delegate() const { return d->mDelegate; } void View::ignoreCurrentChanges(bool ignore) { if (ignore) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged); viewport()->setUpdatesEnabled(false); } else { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); viewport()->setUpdatesEnabled(true); } } void View::ignoreUpdateGeometries(bool ignore) { d->mIgnoreUpdateGeometries = ignore; } bool View::isScrollingLocked() const { // There is another popular requisite: people want the view to automatically // scroll in order to show new arriving mail. This actually makes sense // only when the view is sorted by date and the new mail is (usually) either // appended at the bottom or inserted at the top. It would be also confusing // when the user is browsing some other thread in the meantime. // // So here we make a simple guess: if the view is scrolled somewhere in the // middle then we assume that the user is browsing other threads and we // try to keep the currently selected item steady on the screen. // When the view is "locked" to the top (scrollbar value 0) or to the // bottom (scrollbar value == maximum) then we assume that the user // isn't browsing and we should attempt to show the incoming messages // by keeping the view "locked". // // The "locking" also doesn't make sense in the first big fill view job. // [Well this concept is pre-akonadi. Now the loading is all async anyway... // So all this code is actually triggered during the initial loading, too.] const int scrollBarPosition = verticalScrollBar()->value(); const int scrollBarMaximum = verticalScrollBar()->maximum(); const SortOrder *sortOrder = d->mModel->sortOrder(); const bool lockView = ( // not the first loading job !d->mModel->isLoading() ) && ( // messages sorted by date (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTime) || (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent) ) && ( // scrollbar at top (Descending order) or bottom (Ascending order) (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending) || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending) ); return lockView; } void View::updateGeometries() { if (d->mIgnoreUpdateGeometries || !d->mModel) { return; } const int scrollBarPositionBefore = verticalScrollBar()->value(); const bool lockView = isScrollingLocked(); QTreeView::updateGeometries(); if (lockView) { // we prefer to keep the view locked to the top or bottom if (scrollBarPositionBefore != 0) { // we wanted the view to be locked to the bottom if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) { verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } // else we wanted the view to be locked to top and we shouldn't need to do anything } } StorageModel *View::storageModel() const { return d->mModel->storageModel(); } void View::setAggregation(const Aggregation *aggregation) { d->mAggregation = aggregation; d->mModel->setAggregation(aggregation); // use uniform row heights to speed up, but only if there are no group headers used setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping); } void View::setTheme(Theme *theme) { d->mNeedToApplyThemeColumns = true; d->mTheme = theme; d->mDelegate->setTheme(theme); d->mModel->setTheme(theme); } void View::setSortOrder(const SortOrder *sortOrder) { d->mModel->setSortOrder(sortOrder); } void View::reload() { setStorageModel(storageModel()); } void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) { // This will cause the model to be reset. d->mSaveThemeColumnStateOnSectionResize = false; d->mModel->setStorageModel(storageModel, preSelectionMode); d->mSaveThemeColumnStateOnSectionResize = true; } ////////////////////////////////////////////////////////////////////////////////////////////////////// // Theme column state machinery // // This is yet another beast to beat. The QHeaderView behaviour, at the time of writing, // is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView // and due to its attempts to delay the layout jobs. The delayed layouts, especially, may // cause the widths of the columns to quickly change in an unexpected manner in a place // where previously they have been always settled to the values you set... // // So here we have the tools to: // // - Apply the saved state of the theme columns (applyThemeColumns()). // This function computes the "best fit" state of the visible columns and tries // to apply it to QHeaderView. It also saves the new computed state to the Theme object. // // - Explicitly save the column state, used when the user changes the widths or visibility manually. // This is called through a delayed timer after a column has been resized or used directly // when the visibility state of a column has been changed by toggling a popup menu entry. // // - Display the column state context popup menu and handle its actions // // - Apply the theme columns when the theme changes, when the model changes or when // the widget is resized. // // - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently. // void View::applyThemeColumns() { if (!d->mApplyThemeColumnsTimer) { return; } if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } if (!d->mTheme) { return; } //qCDebug(MESSAGELIST_LOG) << "Apply theme columns"; const QList< Theme::Column * > &columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } if (!viewport()->isVisible()) { return; // invisible } if (viewport()->width() < 1) { return; // insane width } const int viewportWidth = viewport()->width(); d->mLastViewportWidth = viewportWidth; // Now we want to distribute the available width on all the visible columns. // // The rules: // - The visible columns will span the width of the view, if possible. // - The columns with a saved width should take that width. // - The columns on the left should take more space, if possible. // - The columns with no text take just slightly more than their size hint. // while the columns with text take possibly a lot more. // // Note that the first column is always shown (it can't be hidden at all) // The algorithm below is a sort of compromise between: // - Saving the user preferences for widths // - Using exactly the available view space // // It "tends to work" in all cases: // - When there are no user preferences saved and the column widths must be // automatically computed to make best use of available space // - When there are user preferences for only some of the columns // and that should be somewhat preserved while still using all the // available space. // - When all the columns have well defined saved widths int idx = 0; // Gather total size "hint" for visible sections: if the widths of the columns wers // all saved then the total hint is equal to the total saved width. int totalVisibleWidthHint = 0; QVector< int > lColumnSizeHints; for (const auto col : qAsConst(columns)) { if (col->currentlyVisible() || (idx == 0)) { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible"; // Column visible const int savedWidth = col->currentWidth(); const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width(); totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth; lColumnSizeHints.append(hintWidth); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth; } else { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible"; // The column is not visible lColumnSizeHints.append(-1); // dummy } idx++; } if (totalVisibleWidthHint < 16) { totalVisibleWidthHint = 16; // be reasonable } // Now compute somewhat "proportional" widths. idx = 0; QVector< double > lColumnWidths; lColumnWidths.reserve(columns.count()); int totalVisibleWidth = 0; for (const auto col : qAsConst(columns)) { double savedWidth = col->currentWidth(); double hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints.at(idx); double realWidth; if (col->currentlyVisible() || (idx == 0)) { if (col->containsTextItems()) { // the column contains text items, it should get more space (if possible) realWidth = ((hintWidth * viewportWidth) / totalVisibleWidthHint); } else { // the column contains no text items, it should get exactly its hint/saved width. realWidth = hintWidth; } if (realWidth < 2) { realWidth = 2; // don't allow very insane values } totalVisibleWidth += realWidth; } else { // Column not visible realWidth = -1; } lColumnWidths.append(realWidth); idx++; } // Now the algorithm above may be wrong for several reasons... // - We're using fixed widths for certain columns and proportional // for others... // - The user might have changed the width of the view from the // time in that the widths have been saved // - There are some (not well identified) issues with the QTreeView // scrollbar that make our view appear larger or shorter by 2-3 pixels // sometimes. // - ... // So we correct the previous estimates by trying to use exactly // the available space. idx = 0; if (totalVisibleWidth != viewportWidth) { // The estimated widths were not using exactly the available space. if (totalVisibleWidth < viewportWidth) { // We were using less space than available. // Give the additional space to the text columns // also give more space to the first ones and less space to the last ones qreal available = viewportWidth - totalVisibleWidth; for (int idx = 0; idx < columns.count(); ++idx) { Theme::Column *column = columns.at(idx); if ((column->currentlyVisible() || (idx == 0)) && column->containsTextItems()) { // give more space to this column available /= 2; // eat half of the available space lColumnWidths[ idx ] += available; // and give it to this column if (available < 1) { break; // no more space to give away } } } // if any space is still available, give it to the first column if (available >= 1) { lColumnWidths[ 0 ] += available; } } else { // We were using more space than available // If the columns span more than the view then // try to squeeze them in order to make them fit double missing = totalVisibleWidth - viewportWidth; if (missing > 0) { const int count = lColumnWidths.count(); idx = count - 1; while (idx >= 0) { if (columns.at(idx)->currentlyVisible() || (idx == 0)) { double chop = lColumnWidths.at(idx) - lColumnSizeHints.at(idx); if (chop > 0) { if (chop > missing) { chop = missing; } lColumnWidths[ idx ] -= chop; missing -= chop; if (missing < 1) { break; // no more space to recover } } } // else it's invisible idx--; } } } } // We're ready to assign widths. bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections // while resizing them. This is because it has several machineries aimed to delay // the layout to the last possible moment. So if we show a column, it will tend to // screw up the layout of other ones. // We first loop showing/hiding columns then. idx = 0; for (const auto col : qAsConst(columns)) { bool visible = (idx == 0) || col->currentlyVisible(); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible; col->setCurrentlyVisible(visible); header()->setSectionHidden(idx, !visible); idx++; } // Then we loop assigning widths. This is still complicated since QHeaderView tries // very badly to stretch the last section and thus will resize it in the meantime. // But seems to work most of the times... idx = 0; for (const auto col : qAsConst(columns)) { if (col->currentlyVisible()) { const double columnWidth(lColumnWidths.at(idx)); col->setCurrentWidth(columnWidth); //Laurent Bug 358855 - message list column widths lost when program closed // I need to investigate if this code is still necessary (all method) header()->resizeSection(idx, static_cast(columnWidth)); } else { col->setCurrentWidth(-1); } idx++; } idx = 0; bool bTriggeredQtBug = false; for (const auto col : qAsConst(columns)) { if (!header()->isSectionHidden(idx)) { if (!col->currentlyVisible()) { bTriggeredQtBug = true; } } idx++; } setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader); d->mSaveThemeColumnStateOnSectionResize = oldSave; d->mNeedToApplyThemeColumns = false; static bool bAllowRecursion = true; if (bTriggeredQtBug && bAllowRecursion) { bAllowRecursion = false; //qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again"; applyThemeColumns(); bAllowRecursion = true; } } void View::triggerDelayedApplyThemeColumns() { if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } d->mApplyThemeColumnsTimer->setSingleShot(true); d->mApplyThemeColumnsTimer->start(100); } void View::saveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } if (!d->mTheme) { return; } if (d->mNeedToApplyThemeColumns) { return; // don't save the state if it hasn't been applied at all } //qCDebug(MESSAGELIST_LOG) << "Save theme column state"; const auto columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } int idx = 0; for (const auto col : qAsConst(columns)) { if (header()->isSectionHidden(idx)) { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden"; col->setCurrentlyVisible(false); col->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...) } else { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx ); col->setCurrentlyVisible(true); col->setCurrentWidth(header()->sectionSize(idx)); } idx++; } } void View::triggerDelayedSaveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } d->mSaveThemeColumnStateTimer->setSingleShot(true); d->mSaveThemeColumnStateTimer->start(200); } void View::resizeEvent(QResizeEvent *e) { qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")"; QTreeView::resizeEvent(e); if (!isVisible()) { return; // don't play with } if (d->mLastViewportWidth != viewport()->width()) { triggerDelayedApplyThemeColumns(); } if (header()->isVisible()) { return; } // header invisible bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; const int count = header()->count(); if ((count - header()->hiddenSectionCount()) < 2) { // a single column visible: resize it int visibleIndex; for (visibleIndex = 0; visibleIndex < count; visibleIndex++) { if (!header()->isSectionHidden(visibleIndex)) { break; } } if (visibleIndex < count) { header()->resizeSection(visibleIndex, viewport()->width() - 4); } } d->mSaveThemeColumnStateOnSectionResize = oldSave; triggerDelayedSaveThemeColumnState(); } void View::paintEvent(QPaintEvent *event) { #if 0 if (/*mFirstResult &&*/ (!model() || model()->rowCount() == 0)) { QPainter p(viewport()); QFont font = p.font(); font.setItalic(true); p.setFont(font); if (!d->mTextColor.isValid()) { d->generalPaletteChanged(); } p.setPen(d->mTextColor); p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found")); } else { QTreeView::paintEvent(event); } #else QTreeView::paintEvent(event); #endif } void View::modelAboutToEmitLayoutChanged() { // QHeaderView goes totally NUTS with a layoutChanged() call d->mSaveThemeColumnStateOnSectionResize = false; } void View::modelEmittedLayoutChanged() { // This is after a first chunk of work has been done by the model: do apply column states d->mSaveThemeColumnStateOnSectionResize = true; applyThemeColumns(); } void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth) { Q_UNUSED(logicalIndex); Q_UNUSED(oldWidth); Q_UNUSED(newWidth); if (d->mSaveThemeColumnStateOnSectionResize) { triggerDelayedSaveThemeColumnState(); } } int View::sizeHintForColumn(int logicalColumnIndex) const { // QTreeView: please don't touch my column widths... int w = header()->sectionSize(logicalColumnIndex); if (w > 0) { return w; } if (!d->mDelegate) { return 32; // dummy } w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width(); return w; } void View::showEvent(QShowEvent *e) { QTreeView::showEvent(e); } void View::slotHeaderContextMenuRequested(const QPoint &pnt) { if (!d->mTheme) { return; } const auto columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } // the menu for the columns QMenu menu; int idx = 0; for (const auto col : qAsConst(columns)) { QAction *act = menu.addAction(col->label()); act->setCheckable(true); act->setChecked(!header()->isSectionHidden(idx)); if (idx == 0) { act->setEnabled(false); } QObject::connect(act, &QAction::triggered, this, [this, idx] { slotShowHideColumn(idx); }); idx++; } menu.addSeparator(); { QAction *act = menu.addAction(i18n("Adjust Column Sizes")); QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes); } { QAction *act = menu.addAction(i18n("Show Default Columns")); QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns); } menu.addSeparator(); { QAction *act = menu.addAction(i18n("Display Tooltips")); act->setCheckable(true); act->setChecked(MessageListSettings::self()->messageToolTipEnabled()); QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips); } menu.addSeparator(); MessageList::Util::fillViewMenu(&menu, d->mWidget); menu.exec(header()->mapToGlobal(pnt)); } void View::slotAdjustColumnSizes() { if (!d->mTheme) { return; } d->mTheme->resetColumnSizes(); applyThemeColumns(); } void View::slotShowDefaultColumns() { if (!d->mTheme) { return; } d->mTheme->resetColumnState(); applyThemeColumns(); } void View::slotDisplayTooltips(bool showTooltips) { MessageListSettings::self()->setMessageToolTipEnabled(showTooltips); } void View::slotShowHideColumn(int columnIdx) { if (!d->mTheme) { return; // oops } if (columnIdx == 0) { return; // can never be hidden } if (columnIdx >= d->mTheme->columns().count()) { return; } const bool showIt = header()->isSectionHidden(columnIdx); Theme::Column *column = d->mTheme->columns().at(columnIdx); Q_ASSERT(column); // first save column state (as it is, with the column still in previous state) saveThemeColumnState(); // If a section has just been shown, invalidate its width in the skin // since QTreeView assigned it a (possibly insane) default width. // If a section has been hidden, then invalidate its width anyway... // so finally invalidate width always, here. column->setCurrentlyVisible(showIt); column->setCurrentWidth(-1); // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view) applyThemeColumns(); } Item *View::currentItem() const { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return nullptr; } Item *it = static_cast< Item * >(idx.internalPointer()); Q_ASSERT(it); return it; } MessageItem *View::currentMessageItem(bool selectIfNeeded) const { Item *it = currentItem(); if (!it || (it->type() != Item::Message)) { return nullptr; } if (selectIfNeeded) { // Keep things coherent, if the user didn't select it, but acted on it via // a shortcut, do select it now. if (!selectionModel()->isSelected(currentIndex())) { selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); } } return static_cast< MessageItem * >(it); } void View::setCurrentMessageItem(MessageItem *it, bool center) { if (it) { qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject(); const QModelIndex index = d->mModel->index(it, 0); selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select |QItemSelectionModel::Current | QItemSelectionModel::Rows); if (center) { scrollTo(index, QAbstractItemView::PositionAtCenter); } } else { selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current |QItemSelectionModel::Clear); } } bool View::selectionEmpty() const { return selectionModel()->selectedRows().isEmpty(); } QVector< MessageItem * > View::selectionAsMessageItemList(bool includeCollapsedChildren) const { QVector< MessageItem * > selectedMessages; QModelIndexList lSelected = selectionModel()->selectedRows(); if (lSelected.isEmpty()) { return selectedMessages; } for (const auto &idx : qAsConst(lSelected)) { // The asserts below are theoretically valid but at the time // of writing they fail because of a bug in QItemSelectionModel::selectedRows() // which returns also non-selectable items. //Q_ASSERT( selectedItem->type() == Item::Message ); //Q_ASSERT( ( *it ).isValid() ); if (!idx.isValid()) { continue; } Item *selectedItem = static_cast(idx.internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!static_cast(selectedItem)->isValid()) { continue; } Q_ASSERT(!selectedMessages.contains(static_cast(selectedItem))); if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(idx))) { static_cast(selectedItem)->subTreeToList(selectedMessages); } else { selectedMessages.append(static_cast(selectedItem)); } } return selectedMessages; } QVector View::currentThreadAsMessageItemList() const { QVector currentThread; MessageItem *msg = currentMessageItem(); if (!msg) { return currentThread; } while (msg->parent()) { if (msg->parent()->type() != Item::Message) { break; } msg = static_cast< MessageItem * >(msg->parent()); } msg->subTreeToList(currentThread); return currentThread; } void View::setChildrenExpanded(const Item *root, bool expand) { Q_ASSERT(root); auto childList = root->childItems(); if (!childList) { return; } for (const auto child : qAsConst(*childList)) { QModelIndex idx = d->mModel->index(child, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast(idx.internalPointer()) == child); if (expand) { setExpanded(idx, true); if (child->childItemCount() > 0) { setChildrenExpanded(child, true); } } else { if (child->childItemCount() > 0) { setChildrenExpanded(child, false); } setExpanded(idx, false); } } } void View::Private::generalPaletteChanged() { const QPalette palette = q->viewport()->palette(); QColor color = palette.text().color(); color.setAlpha(128); mTextColor = color; } void View::Private::expandFullThread(const QModelIndex &index) { if (!index.isValid()) { return; } Item *item = static_cast< Item * >(index.internalPointer()); if (item->type() != Item::Message) { return; } if (!static_cast< MessageItem * >(item)->parent() || (static_cast< MessageItem * >(item)->parent()->type() != Item::Message)) { q->setChildrenExpanded(item, true); } } void View::setCurrentThreadExpanded(bool expand) { Item *it = currentItem(); if (!it) { return; } if (it->type() == Item::GroupHeader) { setExpanded(currentIndex(), expand); } else if (it->type() == Item::Message) { MessageItem *message = static_cast< MessageItem *>(it); while (message->parent()) { if (message->parent()->type() != Item::Message) { break; } message = static_cast< MessageItem * >(message->parent()); } if (expand) { setExpanded(d->mModel->index(message, 0), true); setChildrenExpanded(message, true); } else { setChildrenExpanded(message, false); setExpanded(d->mModel->index(message, 0), false); } } } void View::setAllThreadsExpanded(bool expand) { scheduleDelayedItemsLayout(); if (d->mAggregation->grouping() == Aggregation::NoGrouping) { // we have no groups so threads start under the root item: just expand/unexpand all setChildrenExpanded(d->mModel->rootItem(), expand); return; } // grouping is in effect: must expand/unexpand one level lower auto childList = d->mModel->rootItem()->childItems(); if (!childList) { return; } for (const auto item : qAsConst(*childList)) { setChildrenExpanded(item, expand); } } void View::setAllGroupsExpanded(bool expand) { if (d->mAggregation->grouping() == Aggregation::NoGrouping) { return; // no grouping in effect } Item *item = d->mModel->rootItem(); auto childList = item->childItems(); if (!childList) { return; } scheduleDelayedItemsLayout(); for (const auto item : qAsConst(*childList)) { Q_ASSERT(item->type() == Item::GroupHeader); QModelIndex idx = d->mModel->index(item, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == item); if (expand) { if (!isExpanded(idx)) { setExpanded(idx, true); } } else { if (isExpanded(idx)) { setExpanded(idx, false); } } } } void View::selectMessageItems(const QVector< MessageItem * > &list) { QItemSelection selection; for (const auto mi : list) { Q_ASSERT(mi); QModelIndex idx = d->mModel->index(mi, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast(idx.internalPointer()) == mi); if (!selectionModel()->isSelected(idx)) { selection.append(QItemSelectionRange(idx)); } ensureDisplayedWithParentsExpanded(mi); } if (!selection.isEmpty()) { selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } static inline bool message_type_matches(Item *item, MessageTypeFilter messageTypeFilter) { switch (messageTypeFilter) { case MessageTypeAny: return true; break; case MessageTypeUnreadOnly: return !item->status().isRead(); break; default: // nothing here break; } // never reached Q_ASSERT(false); return false; } Item *View::messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return nullptr; // no folder } // find the item to start with Item *below; if (referenceItem) { // there was a current item: we start just below it if ( (referenceItem->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || isExpanded(d->mModel->index(referenceItem, 0)) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) below = referenceItem->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(referenceItem->parent()); below = referenceItem->parent()->itemBelowChild(referenceItem); } if (!below) { // reached the end if (loop) { // try re-starting from top below = d->mModel->rootItem()->itemBelow(); Q_ASSERT(below); // must exist (we had a current item) if (below == referenceItem) { return nullptr; // only one item in folder: loop complete } } else { // looping not requested return nullptr; } } } else { // there was no current item, start from beginning below = d->mModel->rootItem()->itemBelow(); if (!below) { return nullptr; // folder empty } } // ok.. now below points to the next message. // While it doesn't satisfy our requirements, go further down QModelIndex parentIndex = d->mModel->index(below->parent(), 0); QModelIndex belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (below->type() != Item::Message) ||// message filter doesn't match (!message_type_matches(below, messageTypeFilter)) ||// is hidden (and we don't want hidden items as they arent "officially" in the view) isRowHidden(belowIndex.row(), parentIndex) ||// is not enabled or not selectable ((d->mModel->flags(belowIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { // find the next one if ((below->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(belowIndex))) { // the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't) below = below->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(below->parent()); below = below->parent()->itemBelowChild(below); } if (!below) { // we reached the end of the folder if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the top: looping makes sense" below = d->mModel->rootItem()->itemBelow(); } // else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit) } else { // looping not requested: nothing more to do return nullptr; } } if (below == referenceItem) { Q_ASSERT(loop); return nullptr; // looped and returned back to the first message } parentIndex = d->mModel->index(below->parent(), 0); belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); } return below; } Item *View::firstMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemAfter(nullptr, messageTypeFilter, false); } Item *View::nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemAfter(currentMessageItem(false), messageTypeFilter, loop); } Item *View::deepestExpandedChild(Item *referenceItem) const { const int children = referenceItem->childItemCount(); if (children > 0 && isExpanded(d->mModel->index(referenceItem, 0))) { return deepestExpandedChild(referenceItem->childItem(children - 1)); } else { return referenceItem; } } Item *View::messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return nullptr; // no folder } // find the item to start with Item *above; if (referenceItem) { Item *parent = referenceItem->parent(); Item *siblingAbove = parent ? parent->itemAboveChild(referenceItem) : nullptr; // there was a current item: we start just above it if ((siblingAbove && siblingAbove != referenceItem && siblingAbove != parent) && (siblingAbove->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || (isExpanded(d->mModel->index(siblingAbove, 0))) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) above = deepestExpandedChild(siblingAbove); } else { // the current item had no children: ask the parent to find the item above Q_ASSERT(referenceItem->parent()); above = referenceItem->parent()->itemAboveChild(referenceItem); } if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // try re-starting from bottom above = d->mModel->rootItem()->deepestItem(); Q_ASSERT(above); // must exist (we had a current item) Q_ASSERT(above != d->mModel->rootItem()); if (above == referenceItem) { return nullptr; // only one item in folder: loop complete } } else { // looping not requested return nullptr; } } } else { // there was no current item, start from end above = d->mModel->rootItem()->deepestItem(); if (!above || !above->parent() || (above == d->mModel->rootItem())) { return nullptr; // folder empty } } // ok.. now below points to the previous message. // While it doesn't satisfy our requirements, go further up QModelIndex parentIndex = d->mModel->index(above->parent(), 0); QModelIndex aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (above->type() != Item::Message) ||// message filter doesn't match (!message_type_matches(above, messageTypeFilter)) ||// we don't expand items but the item has parents unexpanded (so should be skipped) ( // !expand items (messageTypeFilter == MessageTypeAny) &&// has unexpanded parents or is itself hidden (!isDisplayedWithParentsExpanded(above)) ) ||// is hidden isRowHidden(aboveIndex.row(), parentIndex) ||// is not enabled or not selectable ((d->mModel->flags(aboveIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { above = above->itemAbove(); if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the beginning: looping makes sense" above = d->mModel->rootItem()->deepestItem(); } // else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit) } else { // looping not requested: nothing more to do return nullptr; } } if (above == referenceItem) { Q_ASSERT(loop); return nullptr; // looped and returned back to the first message } if (!above->parent()) { return nullptr; } parentIndex = d->mModel->index(above->parent(), 0); aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); } return above; } Item *View::lastMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemBefore(nullptr, messageTypeFilter, false); } Item *View::previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemBefore(currentMessageItem(false), messageTypeFilter, loop); } void View::growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp) { // Qt: why visualIndex() is private? ...I'd really need it here... int selectedVisualCoordinate = visualRect(newSelectedIndex).top(); int topVisualCoordinate = 0xfffffff; // huuuuuge number int bottomVisualCoordinate = -(0xfffffff); int candidate; QModelIndex bottomIndex; QModelIndex topIndex; // find out the actual selection range const QItemSelection selection = selectionModel()->selection(); for (const QItemSelectionRange &range : selection) { // We're asking the model for the index as range.topLeft() and range.bottomRight() // can return indexes in invisible columns which have a null visualRect(). // Column 0, instead, is always visible. QModelIndex top = d->mModel->index(range.top(), 0, range.parent()); QModelIndex bottom = d->mModel->index(range.bottom(), 0, range.parent()); if (top.isValid()) { if (!bottom.isValid()) { bottom = top; } } else { if (!top.isValid()) { top = bottom; } } candidate = visualRect(bottom).bottom(); if (candidate > bottomVisualCoordinate) { bottomVisualCoordinate = candidate; bottomIndex = range.bottomRight(); } candidate = visualRect(top).top(); if (candidate < topVisualCoordinate) { topVisualCoordinate = candidate; topIndex = range.topLeft(); } } if (topIndex.isValid() && bottomIndex.isValid()) { if (movingUp) { if (selectedVisualCoordinate < topVisualCoordinate) { // selecting something above the top: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something below the top: shrink selection const QModelIndexList selectedIndexes = selection.indexes(); for (const QModelIndex &idx : selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() > selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } else { if (selectedVisualCoordinate > bottomVisualCoordinate) { // selecting something below bottom: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something above bottom: shrink selection const QModelIndexList selectedIndexes = selection.indexes(); for (const QModelIndex &idx : selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() < selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } } else { // no existing selection, just grow selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } } bool View::selectNextMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, false); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectPreviousMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, true); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::selectFocusedMessageItem(bool centerItem) { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return; } if (selectionModel()->isSelected(idx)) { return; } selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } } bool View::selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; // nothing to do } Item *it = firstMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); // must never happen (obviously) ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; } Item *it = lastMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::modelFinishedLoading() { Q_ASSERT(storageModel()); Q_ASSERT(!d->mModel->isLoading()); // nothing here for now :) } MessageItemSetReference View::createPersistentSet(const QVector &items) { return d->mModel->createPersistentSet(items); } QList< MessageItem * > View::persistentSetCurrentMessageItemList(MessageItemSetReference ref) { return d->mModel->persistentSetCurrentMessageItemList(ref); } void View::deletePersistentSet(MessageItemSetReference ref) { d->mModel->deletePersistentSet(ref); } void View::markMessageItemsAsAboutToBeRemoved(const QList &items, bool bMark) { if (!bMark) { for (const auto mi : items) { if (mi->isValid()) { // hasn't been removed in the meantime mi->setAboutToBeRemoved(false); } } viewport()->update(); return; } // ok.. we're going to mark the messages as "about to be deleted". // This means that we're going to make them non selectable. // What happens to the selection is generally an untrackable big mess. // Several components and entities are involved. // Qutie tries to apply some kind of internal logic in order to keep // "something" selected and "something" (else) to be current. // The results sometimes appear to depend on the current moon phase. // The Model will do crazy things in order to preserve the current // selection (and possibly the current item). If it's impossible then // it will make its own guesses about what should be selected next. // A problem is that the Model will do it one message at a time. // When item reparenting/reordering is involved then the guesses // can produce non-intuitive results. // Add the fact that selection and current item are distinct concepts, // their relative interaction depends on the settings and is often quite // unclear. // Add the fact that (at the time of writing) several styles don't show // the current item (only Yoda knows why) and this causes some confusion to the user. // Add the fact that the operations are asynchronous: deletion will start // a job, do some event loop processing and then complete the work at a later time. // The Qutie views also tend to accumulate the changes and perform them // all at once at the latest possible stage. // A radical approach is needed: we FIRST deal with the selection // by tring to move it away from the messages about to be deleted // and THEN mark the (hopefully no longer selected) messages as "about to be deleted". // First of all, find out if we're going to clear the entire selection (very likely). bool clearingEntireSelection = true; const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); if (selectedIndexes.count() > items.count()) { // the selection is bigger: we can't clear it completely clearingEntireSelection = false; } else { // the selection has same size or is smaller: we can clear it completely with our removal for (const QModelIndex &selectedIndex : selectedIndexes) { Q_ASSERT(selectedIndex.isValid()); Q_ASSERT(selectedIndex.column() == 0); Item *selectedItem = static_cast< Item * >(selectedIndex.internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!items.contains(static_cast< MessageItem * >(selectedItem))) { // the selection contains something that we aren't going to remove: // we will not clear the selection completely clearingEntireSelection = false; break; } } } if (clearingEntireSelection) { // Try to clear the current selection and select something sensible instead, // so after the deletion we will not end up with a random selection. // Pick up a message in the set (which is very likely to be contiguous), walk the tree // and select the next message that is NOT in the set. MessageItem *aMessage = items.last(); Q_ASSERT(aMessage); // Avoid infinite loops by carrying only a limited number of attempts. // If there is any message that is not in the set then items.count() attempts should find it. int maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *next = messageItemAfter(aMessage, MessageTypeAny, false); if (!next) { // no way aMessage = nullptr; break; } Q_ASSERT(next->type() == Item::Message); aMessage = static_cast< MessageItem * >(next); maxAttempts--; } if (!aMessage) { // try backwards aMessage = items.first(); Q_ASSERT(aMessage); maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *prev = messageItemBefore(aMessage, MessageTypeAny, false); if (!prev) { // no way aMessage = nullptr; break; } Q_ASSERT(prev->type() == Item::Message); aMessage = static_cast< MessageItem * >(prev); maxAttempts--; } } if (aMessage) { QModelIndex aMessageIndex = d->mModel->index(aMessage, 0); Q_ASSERT(aMessageIndex.isValid()); Q_ASSERT(static_cast< MessageItem * >(aMessageIndex.internalPointer()) == aMessage); Q_ASSERT(!selectionModel()->isSelected(aMessageIndex)); setCurrentIndex(aMessageIndex); selectionModel()->select(aMessageIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } } // else we aren't clearing the entire selection so something should just stay selected. // Now mark messages as about to be removed. for (const auto mi : items) { mi->setAboutToBeRemoved(true); QModelIndex idx = d->mModel->index(mi, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< MessageItem * >(idx.internalPointer()) == mi); if (selectionModel()->isSelected(idx)) { selectionModel()->select(idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); } } viewport()->update(); } void View::ensureDisplayedWithParentsExpanded(Item *it) { Q_ASSERT(it); Q_ASSERT(it->parent()); Q_ASSERT(it->isViewable()); // must be attached to the viewable root if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } it = it->parent(); while (it->parent()) { if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == it); if (!isExpanded(idx)) { setExpanded(idx, true); } it = it->parent(); } } bool View::isDisplayedWithParentsExpanded(Item *it) const { // An item is currently viewable iff // - it is marked as viewable in the item structure (that is, qt knows about its existence) // (and this means that all of its parents are marked as viewable) // - it is not explicitly hidden // - all of its parents are expanded if (!it) { return false; // be nice and allow the caller not to care } if (!it->isViewable()) { return false; // item not viewable (not attached to the viewable root or qt not yet aware of it) } // the item and all the parents are marked as viewable. if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { return false; // item qt representation explicitly hidden } // the item (and theoretically all the parents) are not explicitly hidden // check the parent chain it = it->parent(); while (it) { if (it == d->mModel->rootItem()) { return true; // parent is root item: ok } // parent is not root item if (!isExpanded(d->mModel->index(it, 0))) { return false; // parent is not expanded (so child not actually visible) } it = it->parent(); // climb up } // parent hierarchy interrupted somewhere return false; } bool View::isThreaded() const { if (!d->mAggregation) { return false; } return d->mAggregation->threading() != Aggregation::NoThreading; } void View::slotSelectionChanged(const QItemSelection &, const QItemSelection &) { // We assume that when selection changes, current item also changes. QModelIndex current = currentIndex(); if (!current.isValid()) { d->mLastCurrentItem = nullptr; d->mWidget->viewMessageSelected(nullptr); d->mWidget->viewSelectionChanged(); return; } if (!selectionModel()->isSelected(current)) { if (selectedIndexes().count() < 1) { // It may happen after row removals: Model calls this slot on currentIndex() // that actually might have changed "silently", without being selected. QItemSelection selection; selection.append(QItemSelectionRange(current)); selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); return; // the above recurses } else { // something is still selected anyway // This is probably a result of CTRL+Click which unselected current: leave it as it is. return; } } Item *it = static_cast< Item * >(current.internalPointer()); Q_ASSERT(it); switch (it->type()) { case Item::Message: if (d->mLastCurrentItem != it) { qCDebug(MESSAGELIST_LOG) << "View message selected [" << static_cast< MessageItem * >(it)->subject() << "]"; d->mWidget->viewMessageSelected(static_cast< MessageItem * >(it)); d->mLastCurrentItem = it; } break; case Item::GroupHeader: if (d->mLastCurrentItem) { d->mWidget->viewMessageSelected(nullptr); d->mLastCurrentItem = nullptr; } break; default: // should never happen Q_ASSERT(false); break; } d->mWidget->viewSelectionChanged(); } void View::mouseDoubleClickEvent(QMouseEvent *e) { // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } switch (it->type()) { case Item::Message: // Let QTreeView handle the expansion QTreeView::mousePressEvent(e); switch (e->button()) { case Qt::LeftButton: if (d->mDelegate->hitContentItem()) { // Double clicking on clickable icons does NOT activate the message if (d->mDelegate->hitContentItem()->isIcon() && d->mDelegate->hitContentItem()->isClickable()) { return; } } d->mWidget->viewMessageActivated(static_cast< MessageItem * >(it)); break; default: // make gcc happy break; } break; case Item::GroupHeader: // Don't let QTreeView handle the selection (as it deselects the current messages) switch (e->button()) { case Qt::LeftButton: if (it->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } break; default: // make gcc happy break; } break; default: // should never happen Q_ASSERT(false); break; } } void View::changeMessageStatusRead(MessageItem *it, bool read) { Akonadi::MessageStatus set = it->status(); Akonadi::MessageStatus unset = it->status(); if (read) { set.setRead(true); unset.setRead(false); } else { set.setRead(false); unset.setRead(true); } viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::changeMessageStatus(MessageItem *it, Akonadi::MessageStatus set, Akonadi::MessageStatus unset) { // We first change the status of MessageItem itself. This will make the change // visible to the user even if the Model is actually in the middle of a long job (maybe it's loading) // and can't process the status change request immediately. // Here we actually desynchronize the cache and trust that the later call to // d->mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage. // Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next // load the status will be just unchanged: no animals will be harmed. qint32 stat = it->status().toQInt32(); stat |= set.toQInt32(); stat &= ~(unset.toQInt32()); Akonadi::MessageStatus status; status.fromQInt32(stat); it->setStatus(status); // Trigger an update so the immediate change will be shown to the user viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::mousePressEvent(QMouseEvent *e) { d->mMousePressPosition = QPoint(); // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } // Abort any pending message pre-selection as the user is probably // already navigating the view (so pre-selection would make his view jump // to an unexpected place). d->mModel->setPreSelectionMode(PreSelectNone); switch (it->type()) { case Item::Message: d->mMousePressPosition = e->pos(); switch (e->button()) { case Qt::LeftButton: // if we have multi selection then the meaning of hitting // the content item is quite unclear. if (d->mDelegate->hitContentItem() && (selectedIndexes().count() > 1)) { qCDebug(MESSAGELIST_LOG) << "Left hit with selectedIndexes().count() == " << selectedIndexes().count(); switch (d->mDelegate->hitContentItem()->type()) { case Theme::ContentItem::AnnotationIcon: static_cast< MessageItem * >(it)->editAnnotation(this); return; // don't select the item break; case Theme::ContentItem::ActionItemStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isToAct() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusToAct(), it->status().isToAct() ? Akonadi::MessageStatus::statusToAct() : Akonadi::MessageStatus() ); return; // don't select the item break; case Theme::ContentItem::ImportantStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isImportant() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusImportant(), it->status().isImportant() ? Akonadi::MessageStatus::statusImportant() : Akonadi::MessageStatus() ); return; // don't select the item case Theme::ContentItem::ReadStateIcon: changeMessageStatusRead(static_cast< MessageItem * >(it), it->status().isRead() ? false : true); return; break; case Theme::ContentItem::SpamHamStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isSpam() ? Akonadi::MessageStatus() : (it->status().isHam() ? Akonadi::MessageStatus::statusSpam() : Akonadi::MessageStatus::statusHam()), it->status().isSpam() ? Akonadi::MessageStatus::statusSpam() : (it->status().isHam() ? Akonadi::MessageStatus::statusHam() : Akonadi::MessageStatus()) ); return; // don't select the item break; case Theme::ContentItem::WatchedIgnoredStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isIgnored() ? Akonadi::MessageStatus() : (it->status().isWatched() ? Akonadi::MessageStatus::statusIgnored() : Akonadi::MessageStatus::statusWatched()), it->status().isIgnored() ? Akonadi::MessageStatus::statusIgnored() : (it->status().isWatched() ? Akonadi::MessageStatus::statusWatched() : Akonadi::MessageStatus()) ); return; // don't select the item break; default: // make gcc happy break; } } // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); break; case Qt::RightButton: // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); e->accept(); d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } break; case Item::GroupHeader: { // Don't let QTreeView handle the selection (as it deselects the current messages) GroupHeaderItem *groupHeaderItem = static_cast< GroupHeaderItem * >(it); switch (e->button()) { case Qt::LeftButton: { QModelIndex index = d->mModel->index(groupHeaderItem, 0); if (index.isValid()) { setCurrentIndex(index); } if (!d->mDelegate->hitContentItem()) { return; } if (d->mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon) { if (groupHeaderItem->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } } break; } case Qt::RightButton: d->mWidget->viewGroupHeaderContextPopupRequest(groupHeaderItem, viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } break; } default: // should never happen Q_ASSERT(false); break; } } void View::mouseMoveEvent(QMouseEvent *e) { if (!(e->buttons() & Qt::LeftButton)) { QTreeView::mouseMoveEvent(e); return; } if (d->mMousePressPosition.isNull()) { return; } if ((e->pos() - d->mMousePressPosition).manhattanLength() <= QApplication::startDragDistance()) { return; } d->mWidget->viewStartDragRequest(); } #if 0 void View::contextMenuEvent(QContextMenuEvent *e) { Q_UNUSED(e); QModelIndex index = currentIndex(); if (index.isValid()) { QRect indexRect = this->visualRect(index); QPoint pos; if ((indexRect.isValid()) && (indexRect.bottom() > 0)) { if (indexRect.bottom() > viewport()->height()) { if (indexRect.top() <= viewport()->height()) { pos = indexRect.topLeft(); } } else { pos = indexRect.bottomLeft(); } } Item *item = static_cast< Item * >(index.internalPointer()); if (item) { if (item->type() == Item::GroupHeader) { d->mWidget->viewGroupHeaderContextPopupRequest(static_cast< GroupHeaderItem * >(item), viewport()->mapToGlobal(pos)); } else if (!selectionEmpty()) { d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(pos)); e->accept(); } } } } #endif void View::dragEnterEvent(QDragEnterEvent *e) { d->mWidget->viewDragEnterEvent(e); } void View::dragMoveEvent(QDragMoveEvent *e) { d->mWidget->viewDragMoveEvent(e); } void View::dropEvent(QDropEvent *e) { d->mWidget->viewDropEvent(e); } void View::changeEvent(QEvent *e) { switch (e->type()) { case QEvent::FontChange: d->mDelegate->generalFontChanged(); Q_FALLTHROUGH(); case QEvent::PaletteChange: case QEvent::StyleChange: case QEvent::LayoutDirectionChange: case QEvent::LocaleChange: case QEvent::LanguageChange: // All of these affect the theme's internal cache. setTheme(d->mTheme); // A layoutChanged() event will screw up the view state a bit. // Since this is a rare event we just reload the view. reload(); break; default: // make gcc happy by default break; } QTreeView::changeEvent(e); } bool View::event(QEvent *e) { // We catch ToolTip events and pass everything else if (e->type() != QEvent::ToolTip) { return QTreeView::event(e); } if (!MessageListSettings::self()->messageToolTipEnabled()) { return true; // don't display tooltips } QHelpEvent *he = dynamic_cast< QHelpEvent * >(e); if (!he) { return true; // eh ? } QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos())); if (pnt.y() < 0) { return true; // don't display the tooltip for items hidden under the header } QModelIndex idx = indexAt(pnt); if (!idx.isValid()) { return true; // may be } Item *it = static_cast< Item * >(idx.internalPointer()); if (!it) { return true; // hum } Q_ASSERT(storageModel()); QColor bckColor = palette().color(QPalette::ToolTipBase); QColor txtColor = palette().color(QPalette::ToolTipText); QColor darkerColor( ((bckColor.red() * 8) + (txtColor.red() * 2)) / 10, ((bckColor.green() * 8) + (txtColor.green() * 2)) / 10, ((bckColor.blue() * 8) + (txtColor.blue() * 2)) / 10 ); QString bckColorName = bckColor.name(); QString txtColorName = txtColor.name(); QString darkerColorName = darkerColor.name(); const bool textIsLeftToRight = (QApplication::layoutDirection() == Qt::LeftToRight); const QString textDirection = textIsLeftToRight ? QStringLiteral("left") : QStringLiteral("right"); QString tip = QStringLiteral( "" ); switch (it->type()) { case Item::Message: { MessageItem *mi = static_cast< MessageItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName, bckColorName, mi->subject().toHtmlEscaped(), textDirection); tip += QLatin1String( "" \ "" \ "" ); // FIXME: Find a way to show also CC and other header fields ? if (mi->hasChildren()) { Item::ChildItemStats stats; mi->childItemStats(stats); QString statsText; statsText = i18np("%1 reply", "%1 replies", mi->childItemCount()); statsText += QLatin1String(", "); statsText += i18np( "%1 message in subtree (%2 unread)", "%1 messages in subtree (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } break; } case Item::GroupHeader: { GroupHeaderItem *ghi = static_cast< GroupHeaderItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName).arg(bckColorName).arg(ghi->label()).arg(textDirection); QString description; switch (d->mAggregation->grouping()) { case Aggregation::GroupByDate: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) { description = i18nc( "@info:tooltip Formats to something like 'Threads started on 2008-12-21'", "Threads started on %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Threads started Yesterday'", "Threads started %1", ghi->label() ); } break; case Aggregation::MostRecentMessage: description = i18n("Threads with messages dated %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) { if (storageModel()->containsOutboundMessages()) { description = i18nc( "@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", "Messages sent on %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Messages received on 2008-12-21'", "Messages received on %1", ghi->label() ); } } else { if (storageModel()->containsOutboundMessages()) { description = i18nc( "@info:tooltip Formats to something like 'Messages sent Yesterday'", "Messages sent %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Messages received Yesterday'", "Messages received %1", ghi->label() ); } } } break; case Aggregation::GroupByDateRange: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started within %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads containing messages with dates within %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent within %1", ghi->label()); } else { description = i18n("Messages received within %1", ghi->label()); } } break; case Aggregation::GroupBySenderOrReceiver: case Aggregation::GroupBySender: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started by %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message by %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { if (d->mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages sent by %1", ghi->label()); } } else { description = i18n("Messages received from %1", ghi->label()); } } break; case Aggregation::GroupByReceiver: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads directed to %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message directed to %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages received by %1", ghi->label()); } } break; default: // nuthin, make gcc happy break; } if (!description.isEmpty()) { tip += QStringLiteral( "" \ "" \ "" ).arg(description).arg(textDirection); } if (ghi->hasChildren()) { Item::ChildItemStats stats; ghi->childItemStats(stats); QString statsText; if (d->mAggregation->threading() != Aggregation::NoThreading) { statsText = i18np("%1 thread", "%1 threads", ghi->childItemCount()); statsText += QLatin1String(", "); } statsText += i18np( "%1 message (%2 unread)", "%1 messages (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } break; } default: // nuthin (just make gcc happy for now) break; } tip += QLatin1String( "
" \ "
" \ "%3" \ "
" \ "
" \ "" ); const QString htmlCodeForStandardRow = QStringLiteral( "" \ "" \ "" \ ""); if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("From"), mi->displaySender().toHtmlEscaped()); tip += htmlCodeForStandardRow.arg(i18nc("Receiver of the email", "To"), mi->displayReceiver().toHtmlEscaped()); tip += htmlCodeForStandardRow.arg(i18n("Date"), mi->formattedDate()); } else { tip += htmlCodeForStandardRow.arg(mi->displaySender().toHtmlEscaped(), i18n("From")); tip += htmlCodeForStandardRow.arg(mi->displayReceiver().toHtmlEscaped(), i18nc("Receiver of the email", "To")); tip += htmlCodeForStandardRow.arg(mi->formattedDate(), i18n("Date")); } QString status = mi->statusDescription(); const QString tags = mi->tagListDescription(); if (!tags.isEmpty()) { if (!status.isEmpty()) { status += QLatin1String(", "); } status += tags; } if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Status")).arg(status); tip += htmlCodeForStandardRow.arg(i18n("Size")).arg(mi->formattedSize()); + tip += htmlCodeForStandardRow.arg(i18n("Folder")).arg(mi->folder()); } else { tip += htmlCodeForStandardRow.arg(status).arg(i18n("Status")); tip += htmlCodeForStandardRow.arg(mi->formattedSize()).arg(i18n("Size")); + tip += htmlCodeForStandardRow.arg(mi->folder()).arg(i18n("Folder")); } if (mi->hasAnnotation()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Note"), mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
"))); } else { tip += htmlCodeForStandardRow.arg(mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
"))).arg(i18n("Note")); } } QString content = MessageList::Util::contentSummary(mi->akonadiItem()); if (!content.trimmed().isEmpty()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Preview"), content.replace(QLatin1Char('\n'), QStringLiteral("
"))); } else { tip += htmlCodeForStandardRow.arg(content.replace(QLatin1Char('\n'), QStringLiteral("
"))).arg(i18n("Preview")); } } tip += QLatin1String( "
" \ "
" \ "%1:" \ "
" \ "
" \ "%2" \ "
" \ "
" \ "%2" \ "
" \ "
" \ "%3" \ "
" \ "
" \ "%1" \ "
" \ "%2" \ "
" ); QToolTip::showText(he->globalPos(), tip, viewport(), visualRect(idx)); return true; } void View::slotCollapseAllGroups() { setAllGroupsExpanded(false); } void View::slotExpandAllGroups() { setAllGroupsExpanded(true); } void View::slotCollapseCurrentItem() { setCurrentThreadExpanded(false); } void View::slotExpandCurrentItem() { setCurrentThreadExpanded(true); } void View::focusQuickSearch(const QString &selectedText) { d->mWidget->focusQuickSearch(selectedText); } QVector View::currentFilterStatus() const { return d->mWidget->currentFilterStatus(); } MessageList::Core::QuickSearchLine::SearchOptions View::currentOptions() const { return d->mWidget->currentOptions(); } QString View::currentFilterSearchString() const { return d->mWidget->currentFilterSearchString(); } void View::setRowHidden(int row, const QModelIndex &parent, bool hide) { const QModelIndex rowModelIndex = model()->index(row, 0, parent); const Item *const rowItem = static_cast< Item * >(rowModelIndex.internalPointer()); if (rowItem) { const bool currentlyHidden = isRowHidden(row, parent); if (currentlyHidden != hide) { if (currentMessageItem() == rowItem) { selectionModel()->clear(); selectionModel()->clearSelection(); } } } QTreeView::setRowHidden(row, parent, hide); } void View::sortOrderMenuAboutToShow(QMenu *menu) { d->mWidget->sortOrderMenuAboutToShow(menu); } void View::aggregationMenuAboutToShow(QMenu *menu) { d->mWidget->aggregationMenuAboutToShow(menu); } void View::themeMenuAboutToShow(QMenu *menu) { d->mWidget->themeMenuAboutToShow(menu); } void View::setCollapseItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, false); } } void View::setExpandItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, true); } } void View::setQuickSearchClickMessage(const QString &msg) { d->mWidget->quickSearch()->setPlaceholderText(msg); } #include "moc_view.cpp" diff --git a/messagelist/src/storagemodel.cpp b/messagelist/src/storagemodel.cpp index 2c2f5c1a..3a845ed7 100644 --- a/messagelist/src/storagemodel.cpp +++ b/messagelist/src/storagemodel.cpp @@ -1,497 +1,528 @@ /* Copyright (c) 2009 Kevin Ottens 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) any later version. 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "storagemodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "messagelist_debug.h" #include #include "core/messageitem.h" #include "messagelistsettings.h" #include "messagelistutil.h" #include #include #include #include #include #include #include +#include namespace MessageList { class Q_DECL_HIDDEN StorageModel::Private { public: void onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void onSelectionChanged(); void loadSettings(); StorageModel *const q; QAbstractItemModel *mModel = nullptr; QAbstractItemModel *mChildrenFilterModel = nullptr; QItemSelectionModel *mSelectionModel = nullptr; + + QHash mFolderHash; Private(StorageModel *owner) : q(owner) { } }; } // namespace MessageList using namespace Akonadi; using namespace MessageList; namespace { KMime::Message::Ptr messageForItem(const Akonadi::Item &item) { if (!item.hasPayload()) { qCWarning(MESSAGELIST_LOG) << "Not a message" << item.id() << item.remoteId() << item.mimeType(); return KMime::Message::Ptr(); } return item.payload(); } } static QAtomicInt _k_attributeInitialized; StorageModel::StorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent) : Core::StorageModel(parent) , d(new Private(this)) { d->mSelectionModel = selectionModel; if (_k_attributeInitialized.testAndSetAcquire(0, 1)) { AttributeFactory::registerAttribute(); } Akonadi::SelectionProxyModel *childrenFilter = new Akonadi::SelectionProxyModel(d->mSelectionModel, this); childrenFilter->setSourceModel(model); childrenFilter->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection); d->mChildrenFilterModel = childrenFilter; EntityMimeTypeFilterModel *itemFilter = new EntityMimeTypeFilterModel(this); itemFilter->setSourceModel(childrenFilter); itemFilter->addMimeTypeExclusionFilter(Collection::mimeType()); itemFilter->addMimeTypeInclusionFilter(QStringLiteral("message/rfc822")); itemFilter->setHeaderGroup(EntityTreeModel::ItemListHeaders); d->mModel = itemFilter; qCDebug(MESSAGELIST_LOG) << "Using model:" << model->metaObject()->className(); connect(d->mModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &id1, const QModelIndex &id2) { d->onSourceDataChanged(id1, id2); }); connect(d->mModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &StorageModel::layoutAboutToBeChanged); connect(d->mModel, &QAbstractItemModel::layoutChanged, this, &StorageModel::layoutChanged); connect(d->mModel, &QAbstractItemModel::modelAboutToBeReset, this, &StorageModel::modelAboutToBeReset); connect(d->mModel, &QAbstractItemModel::modelReset, this, &StorageModel::modelReset); //Here we assume we'll always get QModelIndex() in the parameters connect(d->mModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &StorageModel::rowsAboutToBeInserted); connect(d->mModel, &QAbstractItemModel::rowsInserted, this, &StorageModel::rowsInserted); connect(d->mModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &StorageModel::rowsAboutToBeRemoved); connect(d->mModel, &QAbstractItemModel::rowsRemoved, this, &StorageModel::rowsRemoved); connect(d->mSelectionModel, &QItemSelectionModel::selectionChanged, this, [this]() { d->onSelectionChanged(); }); d->loadSettings(); connect(MessageListSettings::self(), &MessageListSettings::configChanged, this, [this]() { d->loadSettings(); }); } StorageModel::~StorageModel() { delete d; } Collection::List StorageModel::displayedCollections() const { Collection::List collections; const QModelIndexList indexes = d->mSelectionModel->selectedRows(); for (const QModelIndex &index : indexes) { Collection c = index.data(EntityTreeModel::CollectionRole).value(); if (c.isValid()) { collections << c; } } return collections; } QString StorageModel::id() const { QStringList ids; const QModelIndexList indexes = d->mSelectionModel->selectedRows(); for (const QModelIndex &index : indexes) { Collection c = index.data(EntityTreeModel::CollectionRole).value(); if (c.isValid()) { ids << QString::number(c.id()); } } ids.sort(); return ids.join(QLatin1Char(':')); } bool StorageModel::isOutBoundFolder(const Akonadi::Collection &c) const { if (c.hasAttribute() && c.attribute()->isOutboundFolder()) { return true; } return false; } bool StorageModel::containsOutboundMessages() const { const QModelIndexList indexes = d->mSelectionModel->selectedRows(); for (const QModelIndex &index : indexes) { Collection c = index.data(EntityTreeModel::CollectionRole).value(); if (c.isValid()) { return isOutBoundFolder(c); } } return false; } int StorageModel::initialUnreadRowCountGuess() const { const QModelIndexList indexes = d->mSelectionModel->selectedRows(); int unreadCount = 0; for (const QModelIndex &index : indexes) { Collection c = index.data(EntityTreeModel::CollectionRole).value(); if (c.isValid()) { unreadCount += c.statistics().unreadCount(); } } return unreadCount; } bool StorageModel::initializeMessageItem(MessageList::Core::MessageItem *mi, int row, bool bUseReceiver) const { const Item item = itemForRow(row); const KMime::Message::Ptr mail = messageForItem(item); if (!mail) { return false; } const Collection parentCol = parentCollectionForRow(row); QString sender; if (mail->from()) { sender = mail->from()->asUnicodeString(); } QString receiver; if (mail->to()) { receiver = mail->to()->asUnicodeString(); } // Static for speed reasons static const QString noSubject = i18nc("displayed as subject when the subject of a mail is empty", "No Subject"); static const QString unknown(i18nc("displayed when a mail has unknown sender, receiver or date", "Unknown")); if (sender.isEmpty()) { sender = unknown; } if (receiver.isEmpty()) { receiver = unknown; } mi->initialSetup(mail->date()->dateTime().toSecsSinceEpoch(), item.size(), sender, receiver, bUseReceiver); mi->setItemId(item.id()); mi->setParentCollectionId(parentCol.id()); QString subject = mail->subject()->asUnicodeString(); if (subject.isEmpty()) { subject = QLatin1Char('(') + noSubject + QLatin1Char(')'); } mi->setSubject(subject); + + auto it = d->mFolderHash.find(item.storageCollectionId()); + if (it == d->mFolderHash.end()) { + QString folder; + Collection collection = collectionForId(item.storageCollectionId()); + while (collection.parentCollection().isValid()) { + folder = collection.displayName() + QLatin1Char('/') + folder; + collection = collection.parentCollection(); + } + folder.chop(1); + it = d->mFolderHash.insert(item.storageCollectionId(), folder); + } + mi->setFolder(it.value()); updateMessageItemData(mi, row); return true; } static QByteArray md5Encode(const QByteArray &str) { auto trimmed = str.trimmed(); if (trimmed.isEmpty()) { return QByteArray(); } QCryptographicHash c(QCryptographicHash::Md5); c.addData(trimmed); return c.result(); } static QByteArray md5Encode(const QString &str) { auto trimmed = str.trimmed(); if (trimmed.isEmpty()) { return QByteArray(); } QCryptographicHash c(QCryptographicHash::Md5); c.addData(reinterpret_cast(trimmed.unicode()), sizeof(QChar) * trimmed.length()); return c.result(); } void StorageModel::fillMessageItemThreadingData(MessageList::Core::MessageItem *mi, int row, ThreadingDataSubset subset) const { const KMime::Message::Ptr mail = messageForRow(row); Q_ASSERT(mail); // We ASSUME that initializeMessageItem has been called successfully... switch (subset) { case PerfectThreadingReferencesAndSubject: { const QString subject = mail->subject()->asUnicodeString(); const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes(subject); mi->setStrippedSubjectMD5(md5Encode(strippedSubject)); mi->setSubjectIsPrefixed(subject != strippedSubject); // fall through } Q_FALLTHROUGH(); case PerfectThreadingPlusReferences: { const auto refs = mail->references()->identifiers(); if (!refs.isEmpty()) { mi->setReferencesIdMD5(md5Encode(refs.last())); } } Q_FALLTHROUGH(); // fall through case PerfectThreadingOnly: { mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier())); const auto inReplyTos = mail->inReplyTo()->identifiers(); if (!inReplyTos.isEmpty()) { mi->setInReplyToIdMD5(md5Encode(inReplyTos.first())); } break; } default: Q_ASSERT(false); // should never happen break; } } void StorageModel::updateMessageItemData(MessageList::Core::MessageItem *mi, int row) const { const Item item = itemForRow(row); Akonadi::MessageStatus stat; stat.setStatusFromFlags(item.flags()); mi->setAkonadiItem(item); mi->setStatus(stat); if (stat.isEncrypted()) { mi->setEncryptionState(Core::MessageItem::FullyEncrypted); } else { mi->setEncryptionState(Core::MessageItem::EncryptionStateUnknown); } if (stat.isSigned()) { mi->setSignatureState(Core::MessageItem::FullySigned); } else { mi->setSignatureState(Core::MessageItem::SignatureStateUnknown); } mi->invalidateTagCache(); mi->invalidateAnnotationCache(); } void StorageModel::setMessageItemStatus(MessageList::Core::MessageItem *mi, int row, Akonadi::MessageStatus status) { Q_UNUSED(mi); Item item = itemForRow(row); item.setFlags(status.statusFlags()); ItemModifyJob *job = new ItemModifyJob(item, this); job->disableRevisionCheck(); job->setIgnorePayload(true); } QVariant StorageModel::data(const QModelIndex &index, int role) const { // We don't provide an implementation for data() in No-Akonadi-KMail. // This is because StorageModel must be a wrapper anyway (because columns // must be re-mapped and functions not available in a QAbstractItemModel // are strictly needed. So when porting to Akonadi this class will // either wrap or subclass the MessageModel and implement initializeMessageItem() // with appropriate calls to data(). And for No-Akonadi-KMail we still have // a somewhat efficient implementation. Q_UNUSED(index); Q_UNUSED(role); return QVariant(); } int StorageModel::columnCount(const QModelIndex &parent) const { if (!parent.isValid()) { return 1; } return 0; // this model is flat. } QModelIndex StorageModel::index(int row, int column, const QModelIndex &parent) const { if (!parent.isValid()) { return createIndex(row, column, (void *)nullptr); } return QModelIndex(); // this model is flat. } QModelIndex StorageModel::parent(const QModelIndex &index) const { Q_UNUSED(index); return QModelIndex(); // this model is flat. } int StorageModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->mModel->rowCount(); } return 0; // this model is flat. } QMimeData *StorageModel::mimeData(const QVector< MessageList::Core::MessageItem * > &items) const { QMimeData *data = new QMimeData(); QList urls; urls.reserve(items.count()); for (MessageList::Core::MessageItem *mi : items) { Akonadi::Item item = itemForRow(mi->currentModelIndexRow()); urls << item.url(Item::UrlWithMimeType); } data->setUrls(urls); return data; } void StorageModel::Private::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0)); } void StorageModel::Private::onSelectionChanged() { + mFolderHash.clear(); Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1); } void StorageModel::Private::loadSettings() { // Custom/System colors MessageListSettings *settings = MessageListSettings::self(); if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) { Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor()); Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor()); Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor()); } else { Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor()); Core::MessageItem::setImportantMessageColor(settings->importantMessageColor()); Core::MessageItem::setToDoMessageColor(settings->todoMessageColor()); } if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) { Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); } else { Core::MessageItem::setGeneralFont(settings->messageListFont()); Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont()); Core::MessageItem::setImportantMessageFont(settings->importantMessageFont()); Core::MessageItem::setToDoMessageFont(settings->todoMessageFont()); } } Item StorageModel::itemForRow(int row) const { return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value(); } KMime::Message::Ptr StorageModel::messageForRow(int row) const { return messageForItem(itemForRow(row)); } Collection StorageModel::parentCollectionForRow(int row) const { QAbstractProxyModel *mimeProxy = static_cast(d->mModel); // This is index mapped to Akonadi::SelectionProxyModel const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0)); Q_ASSERT(childrenFilterIndex.isValid()); QAbstractProxyModel *childrenProxy = static_cast(d->mChildrenFilterModel); // This is index mapped to ETM const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex); Q_ASSERT(etmIndex.isValid()); // We cannot possibly refer to top-level collection Q_ASSERT(etmIndex.parent().isValid()); const Collection col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value(); Q_ASSERT(col.isValid()); return col; } +Akonadi::Collection StorageModel::collectionForId(Akonadi::Collection::Id colId) const +{ + // Get ETM + QAbstractProxyModel *childrenProxy = static_cast(d->mChildrenFilterModel); + QAbstractItemModel* etm = childrenProxy->sourceModel(); + + // get index in EntityTreeModel + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(etm, Collection(colId)); + Q_ASSERT(idx.isValid()); + + // get and return collection + return idx.data(EntityTreeModel::CollectionRole).value(); +} + void StorageModel::resetModelStorage() { beginResetModel(); endResetModel(); } #include "moc_storagemodel.cpp" diff --git a/messagelist/src/storagemodel.h b/messagelist/src/storagemodel.h index 2c5e2b2e..d822ced1 100644 --- a/messagelist/src/storagemodel.h +++ b/messagelist/src/storagemodel.h @@ -1,90 +1,92 @@ /* Copyright (c) 2009 Kevin Ottens 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) any later version. 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MESSAGELIST_STORAGEMODEL_H #define MESSAGELIST_STORAGEMODEL_H #include #include #include #include #include #include class QAbstractItemModel; class QItemSelectionModel; namespace Akonadi { class Item; } namespace MessageList { namespace Core { class MessageItem; } /** * The Akonadi specific implementation of the Core::StorageModel. */ class MESSAGELIST_EXPORT StorageModel : public MessageList::Core::StorageModel { Q_OBJECT public: /** * Create a StorageModel wrapping the specified folder. */ explicit StorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent = nullptr); ~StorageModel() override; Q_REQUIRED_RESULT Akonadi::Collection::List displayedCollections() const; Q_REQUIRED_RESULT QString id() const override; Q_REQUIRED_RESULT bool containsOutboundMessages() const override; virtual Q_REQUIRED_RESULT bool isOutBoundFolder(const Akonadi::Collection &c) const; Q_REQUIRED_RESULT int initialUnreadRowCountGuess() const override; Q_REQUIRED_RESULT bool initializeMessageItem(MessageList::Core::MessageItem *mi, int row, bool bUseReceiver) const override; void fillMessageItemThreadingData(MessageList::Core::MessageItem *mi, int row, ThreadingDataSubset subset) const override; void updateMessageItemData(MessageList::Core::MessageItem *mi, int row) const override; void setMessageItemStatus(MessageList::Core::MessageItem *mi, int row, Akonadi::MessageStatus status) override; Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &index) const override; Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; QMimeData *mimeData(const QVector< MessageList::Core::MessageItem * > &) const override; using MessageList::Core::StorageModel::mimeData; Q_REQUIRED_RESULT Akonadi::Item itemForRow(int row) const; Q_REQUIRED_RESULT Akonadi::Collection parentCollectionForRow(int row) const; Q_REQUIRED_RESULT KMime::Message::Ptr messageForRow(int row) const; + + Q_REQUIRED_RESULT Akonadi::Collection collectionForId(Akonadi::Collection::Id colId) const; void resetModelStorage(); private: class Private; Private *const d; }; } // namespace MessageList #endif //!__MESSAGELIST_STORAGEMODEL_H diff --git a/messagelist/src/utils/themeeditor.cpp b/messagelist/src/utils/themeeditor.cpp index 79fd0334..98ca74f2 100644 --- a/messagelist/src/utils/themeeditor.cpp +++ b/messagelist/src/utils/themeeditor.cpp @@ -1,1560 +1,1566 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * This program is free softhisare; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Softhisare Foundation; either version 2 of the License, or * (at your option) any later version. * * 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, write to the Free Softhisare * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "utils/themeeditor.h" #include "core/theme.h" #include "core/groupheaderitem.h" #include "core/messageitem.h" #include "core/modelinvariantrowmapper.h" #include "core/manager.h" #include "utils/comboboxutils.h" #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 #include // for time_t #include #include using namespace MessageList::Utils; using namespace MessageList::Core; static const char gThemeContentItemTypeDndMimeDataFormat[] = "application/x-kmail-messagelistview-theme-contentitem-type"; ThemeColumnPropertiesDialog::ThemeColumnPropertiesDialog(QWidget *parent, Theme::Column *column, const QString &title) : QDialog(parent) , mColumn(column) { QVBoxLayout *mainLayout = new QVBoxLayout(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &ThemeColumnPropertiesDialog::reject); setWindowTitle(title); QWidget *base = new QWidget(this); mainLayout->addWidget(base); mainLayout->addWidget(buttonBox); QGridLayout *g = new QGridLayout(base); QLabel *l = new QLabel(i18nc("@label:textbox Property name", "Name:"), base); g->addWidget(l, 0, 0); mNameEdit = new QLineEdit(base); mNameEdit->setToolTip(i18n("The label that will be displayed in the column header.")); g->addWidget(mNameEdit, 0, 1); l = new QLabel(i18n("Header click sorts messages:"), base); g->addWidget(l, 1, 0); mMessageSortingCombo = new QComboBox(base); mMessageSortingCombo->setToolTip(i18n("The sorting order that clicking on this column header will switch to.")); g->addWidget(mMessageSortingCombo, 1, 1); mVisibleByDefaultCheck = new QCheckBox(i18n("Visible by default"), base); mVisibleByDefaultCheck->setToolTip(i18n("Check this if this column should be visible when the theme is selected.")); g->addWidget(mVisibleByDefaultCheck, 2, 1); mIsSenderOrReceiverCheck = new QCheckBox(i18n("Contains \"Sender or Receiver\" field"), base); mIsSenderOrReceiverCheck->setToolTip(i18n("Check this if this column label should be updated depending on the folder \"inbound\"/\"outbound\" type.")); g->addWidget(mIsSenderOrReceiverCheck, 3, 1); g->setColumnStretch(1, 1); g->setRowStretch(10, 1); connect(okButton, &QPushButton::clicked, this, &ThemeColumnPropertiesDialog::slotOkButtonClicked); // Display the current settings mNameEdit->setText(mColumn->label()); mVisibleByDefaultCheck->setChecked(mColumn->visibleByDefault()); mIsSenderOrReceiverCheck->setChecked(mColumn->isSenderOrReceiver()); ComboBoxUtils::fillIntegerOptionCombo(mMessageSortingCombo, SortOrder::enumerateMessageSortingOptions(Aggregation::PerfectReferencesAndSubject)); ComboBoxUtils::setIntegerOptionComboValue(mMessageSortingCombo, mColumn->messageSorting()); } void ThemeColumnPropertiesDialog::slotOkButtonClicked() { QString text = mNameEdit->text(); if (text.isEmpty()) { text = i18n("Unnamed Column"); } mColumn->setLabel(text); mColumn->setVisibleByDefault(mVisibleByDefaultCheck->isChecked()); mColumn->setIsSenderOrReceiver(mIsSenderOrReceiverCheck->isChecked()); mColumn->setMessageSorting( static_cast< SortOrder::MessageSorting >( ComboBoxUtils::getIntegerOptionComboValue(mMessageSortingCombo, SortOrder::NoMessageSorting) ) ); accept(); } ThemeContentItemSourceLabel::ThemeContentItemSourceLabel(QWidget *parent, Theme::ContentItem::Type type) : QLabel(parent) , mType(type) { setFrameStyle(QFrame::StyledPanel | QFrame::Raised); } ThemeContentItemSourceLabel::~ThemeContentItemSourceLabel() { } MessageList::Core::Theme::ContentItem::Type ThemeContentItemSourceLabel::type() const { return mType; } void ThemeContentItemSourceLabel::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { mMousePressPoint = e->pos(); } } void ThemeContentItemSourceLabel::mouseMoveEvent(QMouseEvent *e) { if (e->buttons() & Qt::LeftButton) { const QPoint diff = mMousePressPoint - e->pos(); if (diff.manhattanLength() > 4) { startDrag(); } } } void ThemeContentItemSourceLabel::startDrag() { //QPixmap pix = QPixmap::grabWidget( this ); //QPixmap alpha( pix.width(), pix.height() ); //alpha.fill(0x0f0f0f0f); //pix.setAlphaChannel( alpha ); // <-- this crashes... no alpha for dragged pixmap :( QMimeData *data = new QMimeData(); QByteArray arry; arry.resize(sizeof(Theme::ContentItem::Type)); *((Theme::ContentItem::Type *)arry.data()) = mType; data->setData(QLatin1String(gThemeContentItemTypeDndMimeDataFormat), arry); QDrag *drag = new QDrag(this); drag->setMimeData(data); //drag->setPixmap( pix ); //drag->setHotSpot( mapFromGlobal( QCursor::pos() ) ); drag->exec(Qt::CopyAction, Qt::CopyAction); } ThemePreviewDelegate::ThemePreviewDelegate(QAbstractItemView *parent) : ThemeDelegate(parent) { mRowMapper = new ModelInvariantRowMapper(); mSampleGroupHeaderItem = new GroupHeaderItem(i18n("Message Group")); mSampleGroupHeaderItem->setDate(time(nullptr)); mSampleGroupHeaderItem->setMaxDate(time(nullptr) + 31337); mSampleGroupHeaderItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long")); mSampleMessageItem = new FakeItem(); mSampleMessageItem->setDate(time(nullptr)); mSampleMessageItem->setSize(0x31337); mSampleMessageItem->setMaxDate(time(nullptr) + 31337); mSampleMessageItem->setSender(i18n("Sender")); mSampleMessageItem->setReceiver(i18n("Receiver")); mSampleMessageItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long")); + mSampleMessageItem->setFolder(i18n("Folder")); mSampleMessageItem->setSignatureState(MessageItem::FullySigned); mSampleMessageItem->setEncryptionState(MessageItem::FullyEncrypted); QList< MessageItem::Tag * > list; list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 1"), QString())); list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 2"), QString())); list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 3"), QString())); mSampleMessageItem->setFakeTags(list); mRowMapper->createModelInvariantIndex(0, mSampleMessageItem); mSampleGroupHeaderItem->rawAppendChildItem(mSampleMessageItem); mSampleMessageItem->setParent(mSampleGroupHeaderItem); Akonadi::MessageStatus stat; stat.fromQInt32(0x7fffffff); stat.setQueued(false); stat.setSent(false); stat.setSpam(true); stat.setWatched(true); stat.setHasInvitation(); //stat.setHasAttachment( false ); mSampleMessageItem->setStatus(stat); } ThemePreviewDelegate::~ThemePreviewDelegate() { delete mSampleGroupHeaderItem; //delete mSampleMessageItem; (deleted by the parent) delete mRowMapper; } Item *ThemePreviewDelegate::itemFromIndex(const QModelIndex &index) const { if (index.parent().isValid()) { return mSampleMessageItem; } return mSampleGroupHeaderItem; } ThemePreviewWidget::ThemePreviewWidget(QWidget *parent) : QTreeWidget(parent) , mTheme(nullptr) { mSelectedThemeContentItem = nullptr; mSelectedThemeColumn = nullptr; mFirstShow = true; mReadOnly = false; mDelegate = new ThemePreviewDelegate(this); setItemDelegate(mDelegate); setRootIsDecorated(false); viewport()->setAcceptDrops(true); header()->setContextMenuPolicy(Qt::CustomContextMenu); // make sure it's true connect(header(), &QWidget::customContextMenuRequested, this, &ThemePreviewWidget::slotHeaderContextMenuRequested); mGroupHeaderSampleItem = new QTreeWidgetItem(this); mGroupHeaderSampleItem->setText(0, QString()); mGroupHeaderSampleItem->setFlags(Qt::ItemIsEnabled); QTreeWidgetItem *m = new QTreeWidgetItem(mGroupHeaderSampleItem); m->setText(0, QString()); mGroupHeaderSampleItem->setExpanded(true); header()->setSectionsMovable(false); } void ThemePreviewWidget::changeEvent(QEvent *event) { if (event->type() == QEvent::FontChange) { mDelegate->generalFontChanged(); } QTreeWidget::changeEvent(event); } ThemePreviewWidget::~ThemePreviewWidget() { } QSize ThemePreviewWidget::sizeHint() const { return QSize(350, 180); } void ThemePreviewWidget::setReadOnly(bool readOnly) { mReadOnly = readOnly; } void ThemePreviewWidget::applyThemeColumnWidths() { if (!mTheme) { return; } const QList< Theme::Column * > &columns = mTheme->columns(); if (columns.isEmpty()) { viewport()->update(); // trigger a repaint return; } // Now we want to distribute the available width on all the columns. // The algorithm used here is very similar to the one used in View::applyThemeColumns(). // It just takes care of ALL the columns instead of the visible ones. QList< Theme::Column * >::ConstIterator it; // Gather size hints for all sections. int idx = 0; int totalVisibleWidthHint = 0; QList< Theme::Column * >::ConstIterator end(columns.constEnd()); for (it = columns.constBegin(); it != end; ++it) { totalVisibleWidthHint += mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width(); idx++; } if (totalVisibleWidthHint < 16) { totalVisibleWidthHint = 16; // be reasonable } // Now we can compute proportional widths. idx = 0; QList< int > realWidths; realWidths.reserve(columns.count()); int totalVisibleWidth = 0; end = columns.constEnd(); for (it = columns.constBegin(); it != end; ++it) { int hintWidth = mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width(); int realWidth; if ((*it)->containsTextItems()) { // the column contains text items, it should get more space realWidth = ((hintWidth * viewport()->width()) / totalVisibleWidthHint) - 2; // -2 is heuristic if (realWidth < (hintWidth + 2)) { realWidth = hintWidth + 2; // can't be less } } else { // the column contains no text items, it should get just a little bit more than its sizeHint(). realWidth = hintWidth + 2; } realWidths.append(realWidth); totalVisibleWidth += realWidth; idx++; } idx = 0; totalVisibleWidth += 4; // account for some view's border if (totalVisibleWidth < viewport()->width()) { // give the additional space to the text columns // also give more space to the first ones and less space to the last ones int available = viewport()->width() - totalVisibleWidth; for (it = columns.begin(); it != columns.end(); ++it) { if (((*it)->visibleByDefault() || (idx == 0)) && (*it)->containsTextItems()) { // give more space to this column available >>= 1; // eat half of the available space realWidths[ idx ] += available; // and give it to this column } idx++; } // if any space is still available, give it to the first column if (available) { realWidths[ 0 ] += available; } } idx = 0; // We're ready. // Assign widths. Hide the sections that are not visible by default, show the other ones. for (it = columns.begin(); it != columns.end(); ++it) { header()->resizeSection(idx, realWidths[ idx ]); idx++; } #if 0 if (mTheme->viewHeaderPolicy() == Theme::NeverShowHeader) { header()->hide(); } else { header()->show(); } #endif } void ThemePreviewWidget::setTheme(Theme *theme) { bool themeChanged = theme != mTheme; mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = QPoint(); mDropIndicatorPoint2 = QPoint(); mTheme = theme; mDelegate->setTheme(theme); if (!mTheme) { return; } mGroupHeaderSampleItem->setExpanded(true); const QList< Theme::Column * > &columns = mTheme->columns(); setColumnCount(columns.count()); QStringList headerLabels; headerLabels.reserve(columns.count()); QList< Theme::Column * >::ConstIterator end(columns.constEnd()); for (QList< Theme::Column * >::ConstIterator it = columns.constBegin(); it != end; ++it) { QString label = (*it)->label(); if ((*it)->visibleByDefault()) { label += QStringLiteral(" (%1)").arg(i18nc("Indicates whether or not a header label is visible", "Visible")); } headerLabels.append(label); } setHeaderLabels(headerLabels); if (themeChanged) { applyThemeColumnWidths(); } viewport()->update(); // trigger a repaint } void ThemePreviewWidget::internalHandleDragEnterEvent(QDragEnterEvent *e) { e->ignore(); if (!e->mimeData()) { return; } if (!e->mimeData()->hasFormat(QLatin1String(gThemeContentItemTypeDndMimeDataFormat))) { return; } e->accept(); } void ThemePreviewWidget::showEvent(QShowEvent *e) { QTreeWidget::showEvent(e); if (mFirstShow) { // Make sure we re-apply column widths the first time we're shown. // The first "apply" call was made while the widget was still hidden and // almost surely had wrong sizes. applyThemeColumnWidths(); mFirstShow = false; } } void ThemePreviewWidget::dragEnterEvent(QDragEnterEvent *e) { internalHandleDragEnterEvent(e); mThemeSelectedContentItemRect = QRect(); viewport()->update(); // trigger a repaint } void ThemePreviewWidget::internalHandleDragMoveEvent(QDragMoveEvent *e) { e->ignore(); if (mReadOnly) { return; } if (!e->mimeData()) { return; } if (!e->mimeData()->hasFormat(QLatin1String(gThemeContentItemTypeDndMimeDataFormat))) { return; } QByteArray arry = e->mimeData()->data(QLatin1String(gThemeContentItemTypeDndMimeDataFormat)); if (arry.size() != sizeof(Theme::ContentItem::Type)) { return; // ugh } Theme::ContentItem::Type type = *((Theme::ContentItem::Type *)arry.data()); if (!computeContentItemInsertPosition(e->pos(), type)) { return; } e->accept(); } void ThemePreviewWidget::dragMoveEvent(QDragMoveEvent *e) { if (mReadOnly) { return; } internalHandleDragMoveEvent(e); mThemeSelectedContentItemRect = QRect(); viewport()->update(); // trigger a repaint } void ThemePreviewWidget::dropEvent(QDropEvent *e) { mDropIndicatorPoint1 = mDropIndicatorPoint2; e->ignore(); if (mReadOnly) { return; } if (!e->mimeData()) { return; } if (!e->mimeData()->hasFormat(QLatin1String(gThemeContentItemTypeDndMimeDataFormat))) { return; } QByteArray arry = e->mimeData()->data(QLatin1String(gThemeContentItemTypeDndMimeDataFormat)); if (arry.size() != sizeof(Theme::ContentItem::Type)) { return; // ugh } Theme::ContentItem::Type type = *((Theme::ContentItem::Type *)arry.data()); if (!computeContentItemInsertPosition(e->pos(), type)) { viewport()->update(); return; } Theme::Row *row = nullptr; switch (mRowInsertPosition) { case AboveRow: row = new Theme::Row(); if (mDelegate->hitItem()->type() == Item::Message) { const_cast< Theme::Column * >(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex(), row); } else { const_cast< Theme::Column * >(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex(), row); } break; case InsideRow: row = const_cast< Theme::Row * >(mDelegate->hitRow()); break; case BelowRow: row = new Theme::Row(); if (mDelegate->hitItem()->type() == Item::Message) { const_cast< Theme::Column * >(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex() + 1, row); } else { const_cast< Theme::Column * >(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex() + 1, row); } break; } if (!row) { return; } Theme::ContentItem *ci = new Theme::ContentItem(type); if (ci->canBeDisabled()) { if (ci->isClickable()) { ci->setSoftenByBlendingWhenDisabled(true); // default to softened } else { ci->setHideWhenDisabled(true); // default to hidden } } int idx; switch (mItemInsertPosition) { case OnLeftOfItem: if (!mDelegate->hitContentItem()) { // bleah delete ci; return; } idx = mDelegate->hitContentItemRight() \ ? row->rightItems().indexOf(const_cast< Theme::ContentItem * >(mDelegate->hitContentItem())) \ : row->leftItems().indexOf(const_cast< Theme::ContentItem * >(mDelegate->hitContentItem())); if (idx < 0) { // bleah delete ci; return; } if (mDelegate->hitContentItemRight()) { row->insertRightItem(idx + 1, ci); } else { row->insertLeftItem(idx, ci); } break; case OnRightOfItem: if (!mDelegate->hitContentItem()) { // bleah delete ci; return; } idx = mDelegate->hitContentItemRight() \ ? row->rightItems().indexOf(const_cast< Theme::ContentItem * >(mDelegate->hitContentItem())) \ : row->leftItems().indexOf(const_cast< Theme::ContentItem * >(mDelegate->hitContentItem())); if (idx < 0) { // bleah delete ci; return; } if (mDelegate->hitContentItemRight()) { row->insertRightItem(idx, ci); } else { row->insertLeftItem(idx + 1, ci); } break; case AsLastLeftItem: row->addLeftItem(ci); break; case AsLastRightItem: row->addRightItem(ci); break; case AsFirstLeftItem: row->insertLeftItem(0, ci); break; case AsFirstRightItem: row->insertRightItem(0, ci); break; default: // should never happen row->addRightItem(ci); break; } e->acceptProposedAction(); mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = mDropIndicatorPoint2; mSelectedThemeContentItem = nullptr; setTheme(mTheme); // this will reset theme cache and trigger a global update } bool ThemePreviewWidget::computeContentItemInsertPosition(const QPoint &pos, Theme::ContentItem::Type type) { mDropIndicatorPoint1 = mDropIndicatorPoint2; // this marks the position as invalid if (!mDelegate->hitTest(pos, false)) { return false; } if (!mDelegate->hitRow()) { return false; } if (mDelegate->hitRowIsMessageRow()) { if (!Theme::ContentItem::applicableToMessageItems(type)) { return false; } } else { if (!Theme::ContentItem::applicableToGroupHeaderItems(type)) { return false; } } QRect rowRect = mDelegate->hitRowRect(); if (pos.y() < rowRect.top() + 3) { // above a row mRowInsertPosition = AboveRow; if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) { mDropIndicatorPoint1 = rowRect.topLeft(); mItemInsertPosition = AsLastLeftItem; } else { mDropIndicatorPoint1 = rowRect.topRight(); mItemInsertPosition = AsLastRightItem; } mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.top()); return true; } if (pos.y() > rowRect.bottom() - 3) { // below a row mRowInsertPosition = BelowRow; if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) { mDropIndicatorPoint1 = rowRect.bottomLeft(); mItemInsertPosition = AsLastLeftItem; } else { mDropIndicatorPoint1 = rowRect.bottomRight(); mItemInsertPosition = AsLastRightItem; } mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.bottom()); return true; } mRowInsertPosition = InsideRow; if (!mDelegate->hitContentItem()) { // didn't hit anything... probably no items in the row if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) { mItemInsertPosition = AsLastLeftItem; mDropIndicatorPoint1 = QPoint(rowRect.left(), rowRect.top()); mDropIndicatorPoint2 = QPoint(rowRect.left(), rowRect.bottom()); } else { mItemInsertPosition = AsLastRightItem; mDropIndicatorPoint1 = QPoint(rowRect.right(), rowRect.top()); mDropIndicatorPoint2 = QPoint(rowRect.right(), rowRect.bottom()); } return true; } // hit something, maybe inexactly QRect itemRect = mDelegate->hitContentItemRect(); if (!itemRect.contains(pos)) { // inexact hit: outside an item if (pos.x() > itemRect.right()) { // right side of an item if (mDelegate->hitRow()->rightItems().count() < 1) { // between the last left item and the right side if (pos.x() > (itemRect.right() + ((rowRect.right() - itemRect.right()) / 2))) { // first/last right item mItemInsertPosition = AsFirstRightItem; mDropIndicatorPoint1 = rowRect.topRight(); mDropIndicatorPoint2 = rowRect.bottomRight(); } return true; } // either there were some right items (so the theme delegate knows that the reported item is the closest) // or there were no right items but the position is closest to the left item than the right row end mItemInsertPosition = OnRightOfItem; mDropIndicatorPoint1 = itemRect.topRight(); mDropIndicatorPoint2 = itemRect.bottomRight(); return true; } // left side of an item if (mDelegate->hitRow()->leftItems().count() < 1) { // between the left side and the leftmost right item if (pos.x() < (itemRect.left() - ((itemRect.left() - rowRect.left()) / 2))) { mItemInsertPosition = AsFirstLeftItem; mDropIndicatorPoint1 = rowRect.topLeft(); mDropIndicatorPoint2 = rowRect.bottomLeft(); return true; } } mItemInsertPosition = OnLeftOfItem; mDropIndicatorPoint1 = itemRect.topLeft(); mDropIndicatorPoint2 = itemRect.bottomLeft(); return true; } // exact hit if (pos.x() < (itemRect.left() + (itemRect.width() / 2))) { // left side mItemInsertPosition = OnLeftOfItem; mDropIndicatorPoint1 = itemRect.topLeft(); mDropIndicatorPoint2 = itemRect.bottomLeft(); return true; } // right side mItemInsertPosition = OnRightOfItem; mDropIndicatorPoint1 = itemRect.topRight(); mDropIndicatorPoint2 = itemRect.bottomRight(); return true; } void ThemePreviewWidget::mouseMoveEvent(QMouseEvent *e) { if (!(mSelectedThemeContentItem && (e->buttons() & Qt::LeftButton)) || mReadOnly) { QTreeWidget::mouseMoveEvent(e); return; } if (mSelectedThemeContentItem != mDelegate->hitContentItem()) { QTreeWidget::mouseMoveEvent(e); return; // ugh.. something weird happened } // starting a drag ? const QPoint diff = e->pos() - mMouseDownPoint; if (diff.manhattanLength() <= 4) { QTreeWidget::mouseMoveEvent(e); return; // ugh.. something weird happened } // starting a drag QMimeData *data = new QMimeData(); QByteArray arry; arry.resize(sizeof(Theme::ContentItem::Type)); *((Theme::ContentItem::Type *)arry.data()) = mSelectedThemeContentItem->type(); data->setData(QLatin1String(gThemeContentItemTypeDndMimeDataFormat), arry); QDrag *drag = new QDrag(this); drag->setMimeData(data); // remove the Theme::ContentItem from the Theme if (mDelegate->hitContentItemRight()) { const_cast< Theme::Row * >(mDelegate->hitRow())->removeRightItem(mSelectedThemeContentItem); } else { const_cast< Theme::Row * >(mDelegate->hitRow())->removeLeftItem(mSelectedThemeContentItem); } delete mSelectedThemeContentItem; if (mDelegate->hitRow()->rightItems().isEmpty() && mDelegate->hitRow()->leftItems().isEmpty()) { if (mDelegate->hitItem()->type() == Item::Message) { if (mDelegate->hitColumn()->messageRows().count() > 1) { const_cast< Theme::Column * >(mDelegate->hitColumn())->removeMessageRow(const_cast< Theme::Row * >(mDelegate->hitRow())); delete mDelegate->hitRow(); } } else { if (mDelegate->hitColumn()->groupHeaderRows().count() > 1) { const_cast< Theme::Column * >(mDelegate->hitColumn())->removeGroupHeaderRow(const_cast< Theme::Row * >(mDelegate->hitRow())); delete mDelegate->hitRow(); } } } mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = mDropIndicatorPoint2; setTheme(mTheme); // this will reset theme cache and trigger a global update // and do drag drag->exec(Qt::CopyAction, Qt::CopyAction); } void ThemePreviewWidget::mousePressEvent(QMouseEvent *e) { if (mReadOnly) { QTreeWidget::mousePressEvent(e); return; } mMouseDownPoint = e->pos(); if (mDelegate->hitTest(mMouseDownPoint)) { mSelectedThemeContentItem = const_cast< Theme::ContentItem * >(mDelegate->hitContentItem()); mThemeSelectedContentItemRect = mSelectedThemeContentItem ? mDelegate->hitContentItemRect() : QRect(); } else { mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); } QTreeWidget::mousePressEvent(e); viewport()->update(); if (e->button() == Qt::RightButton) { QMenu menu; if (mSelectedThemeContentItem) { menu.addSection(Theme::ContentItem::description(mSelectedThemeContentItem->type())); if (mSelectedThemeContentItem->displaysText()) { QAction *act = menu.addAction(i18nc("@action:inmenu soften the text color", "Soften")); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->softenByBlending()); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotSoftenActionTriggered); QMenu *childmenu = new QMenu(&menu); act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Bold")); act->setData(QVariant(static_cast(Theme::ContentItem::IsBold))); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->isBold()); act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Italic")); act->setData(QVariant(static_cast(Theme::ContentItem::IsItalic))); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->isItalic()); connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotFontMenuTriggered); menu.addMenu(childmenu)->setText(i18n("Font")); } if (mSelectedThemeContentItem->canUseCustomColor()) { QMenu *childmenu = new QMenu(&menu); QActionGroup *grp = new QActionGroup(childmenu); QAction *act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Default")); act->setData(QVariant(static_cast< int >(0))); act->setCheckable(true); act->setChecked(!mSelectedThemeContentItem->useCustomColor()); grp->addAction(act); act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Custom...")); act->setData(QVariant(static_cast< int >(Theme::ContentItem::UseCustomColor))); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->useCustomColor()); grp->addAction(act); connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotForegroundColorMenuTriggered); menu.addMenu(childmenu)->setText(i18n("Foreground Color")); } if (mSelectedThemeContentItem->canBeDisabled()) { QMenu *childmenu = new QMenu(&menu); QActionGroup *grp = new QActionGroup(childmenu); QAction *act = childmenu->addAction(i18nc("Hide a mark if the mail does not have the attribute, e.g. Important mark on a non important mail", "Hide")); act->setData(QVariant(static_cast< int >(Theme::ContentItem::HideWhenDisabled))); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->hideWhenDisabled()); grp->addAction(act); act = childmenu->addAction(i18nc("Keep a empty space in the list if the mail does not have the attribute, e.g. Important mark on a non important mail", "Keep Empty Space")); act->setData(QVariant(static_cast< int >(0))); act->setCheckable(true); act->setChecked(!(mSelectedThemeContentItem->softenByBlendingWhenDisabled() || mSelectedThemeContentItem->hideWhenDisabled())); grp->addAction(act); act = childmenu->addAction(i18nc("Show the icon softened in the list if the mail does not have the attribute, e.g. Important mark on a non important mail", "Keep Softened Icon")); act->setData(QVariant(static_cast< int >(Theme::ContentItem::SoftenByBlendingWhenDisabled))); act->setCheckable(true); act->setChecked(mSelectedThemeContentItem->softenByBlendingWhenDisabled()); grp->addAction(act); connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotDisabledFlagsMenuTriggered); menu.addMenu(childmenu)->setText(i18n("When Disabled")); } } if (mDelegate->hitItem()) { if (mDelegate->hitItem()->type() == Item::GroupHeader) { menu.addSection(i18n("Group Header")); // Background color (mode) submenu QMenu *childmenu = new QMenu(&menu); QActionGroup *grp = new QActionGroup(childmenu); QAction *act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "None")); act->setData(QVariant(static_cast< int >(Theme::Transparent))); act->setCheckable(true); act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::Transparent); grp->addAction(act); act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Automatic")); act->setData(QVariant(static_cast< int >(Theme::AutoColor))); act->setCheckable(true); act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::AutoColor); grp->addAction(act); act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Custom...")); act->setData(QVariant(static_cast< int >(Theme::CustomColor))); act->setCheckable(true); act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::CustomColor); grp->addAction(act); connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered); menu.addMenu(childmenu)->setText(i18n("Background Color")); // Background style submenu childmenu = new QMenu(&menu); grp = new QActionGroup(childmenu); QVector< QPair< QString, int > > styles = Theme::enumerateGroupHeaderBackgroundStyles(); QVector< QPair< QString, int > >::ConstIterator end(styles.constEnd()); for (QVector< QPair< QString, int > >::ConstIterator it = styles.constBegin(); it != end; ++it) { act = childmenu->addAction((*it).first); act->setData(QVariant((*it).second)); act->setCheckable(true); act->setChecked(mTheme->groupHeaderBackgroundStyle() == static_cast< Theme::GroupHeaderBackgroundStyle >((*it).second)); grp->addAction(act); } connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered); act = menu.addMenu(childmenu); act->setText(i18n("Background Style")); if (mTheme->groupHeaderBackgroundMode() == Theme::Transparent) { act->setEnabled(false); } } } if (menu.isEmpty()) { return; } menu.exec(viewport()->mapToGlobal(e->pos())); } } void ThemePreviewWidget::slotDisabledFlagsMenuTriggered(QAction *act) { if (!mSelectedThemeContentItem) { return; } bool ok; const int flags = act->data().toInt(&ok); if (!ok) { return; } mSelectedThemeContentItem->setHideWhenDisabled(flags == Theme::ContentItem::HideWhenDisabled); mSelectedThemeContentItem->setSoftenByBlendingWhenDisabled(flags == Theme::ContentItem::SoftenByBlendingWhenDisabled); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotForegroundColorMenuTriggered(QAction *act) { if (!mSelectedThemeContentItem) { return; } bool ok; const int flag = act->data().toInt(&ok); if (!ok) { return; } if (flag == 0) { mSelectedThemeContentItem->setUseCustomColor(false); setTheme(mTheme); // this will reset theme cache and trigger a global update return; } QColor clr; clr = QColorDialog::getColor(mSelectedThemeContentItem->customColor(), this); if (!clr.isValid()) { return; } mSelectedThemeContentItem->setCustomColor(clr); mSelectedThemeContentItem->setUseCustomColor(true); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotSoftenActionTriggered(bool) { if (!mSelectedThemeContentItem) { return; } mSelectedThemeContentItem->setSoftenByBlending(!mSelectedThemeContentItem->softenByBlending()); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotFontMenuTriggered(QAction *act) { if (!mSelectedThemeContentItem) { return; } bool ok; const int flag = act->data().toInt(&ok); if (!ok) { return; } if (flag == Theme::ContentItem::IsBold && mSelectedThemeContentItem->isBold() != act->isChecked()) { mSelectedThemeContentItem->setBold(act->isChecked()); setTheme(mTheme); } else if (flag == Theme::ContentItem::IsItalic && mSelectedThemeContentItem->isItalic() != act->isChecked()) { mSelectedThemeContentItem->setItalic(act->isChecked()); setTheme(mTheme); } } void ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered(QAction *act) { bool ok; Theme::GroupHeaderBackgroundMode mode = static_cast< Theme::GroupHeaderBackgroundMode >(act->data().toInt(&ok)); if (!ok) { return; } switch (mode) { case Theme::Transparent: mTheme->setGroupHeaderBackgroundMode(Theme::Transparent); break; case Theme::AutoColor: mTheme->setGroupHeaderBackgroundMode(Theme::AutoColor); break; case Theme::CustomColor: { QColor clr; clr = QColorDialog::getColor(mTheme->groupHeaderBackgroundColor(), this); if (!clr.isValid()) { return; } mTheme->setGroupHeaderBackgroundMode(Theme::CustomColor); mTheme->setGroupHeaderBackgroundColor(clr); break; } } setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered(QAction *act) { bool ok; Theme::GroupHeaderBackgroundStyle mode = static_cast< Theme::GroupHeaderBackgroundStyle >(act->data().toInt(&ok)); if (!ok) { return; } mTheme->setGroupHeaderBackgroundStyle(mode); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::paintEvent(QPaintEvent *e) { QTreeWidget::paintEvent(e); if ( mThemeSelectedContentItemRect.isValid() || (mDropIndicatorPoint1 != mDropIndicatorPoint2) ) { QPainter painter(viewport()); if (mThemeSelectedContentItemRect.isValid()) { painter.setPen(QPen(Qt::black)); painter.drawRect(mThemeSelectedContentItemRect); } if (mDropIndicatorPoint1 != mDropIndicatorPoint2) { painter.setPen(QPen(Qt::black, 3)); painter.drawLine(mDropIndicatorPoint1, mDropIndicatorPoint2); } } } void ThemePreviewWidget::slotHeaderContextMenuRequested(const QPoint &pos) { if (mReadOnly) { return; } QTreeWidgetItem *hitem = headerItem(); if (!hitem) { return; // ooops } int col = header()->logicalIndexAt(pos); if (col < 0) { return; } if (col >= mTheme->columns().count()) { return; } mSelectedThemeColumn = mTheme->column(col); if (!mSelectedThemeColumn) { return; } QMenu menu; menu.setTitle(mSelectedThemeColumn->label()); QAction *act = menu.addAction(i18n("Column Properties...")); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotColumnProperties); act = menu.addAction(i18n("Add Column...")); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotAddColumn); act = menu.addAction(i18n("Delete Column")); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotDeleteColumn); act->setEnabled(col > 0); menu.addSeparator(); act = menu.addAction(i18n("Move Column to Left")); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToLeft); act->setEnabled(col > 0); act = menu.addAction(i18n("Move Column to Right")); connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToRight); act->setEnabled(col < mTheme->columns().count() - 1); menu.exec(header()->mapToGlobal(pos)); } void ThemePreviewWidget::slotMoveColumnToLeft() { if (!mSelectedThemeColumn) { return; } const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn); mTheme->moveColumn(columnIndex, columnIndex - 1); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotMoveColumnToRight() { if (!mSelectedThemeColumn) { return; } const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn); mTheme->moveColumn(columnIndex, columnIndex + 1); setTheme(mTheme); // this will reset theme cache and trigger a global update } void ThemePreviewWidget::slotAddColumn() { int newColumnIndex = mTheme->columns().count(); if (mSelectedThemeColumn) { newColumnIndex = mTheme->columns().indexOf(mSelectedThemeColumn); if (newColumnIndex < 0) { newColumnIndex = mTheme->columns().count(); } else { newColumnIndex++; } } mSelectedThemeColumn = new Theme::Column(); mSelectedThemeColumn->setLabel(i18n("New Column")); mSelectedThemeColumn->setVisibleByDefault(true); mSelectedThemeColumn->addMessageRow(new Theme::Row()); mSelectedThemeColumn->addGroupHeaderRow(new Theme::Row()); ThemeColumnPropertiesDialog *dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Add New Column")); if (dlg->exec() == QDialog::Accepted) { mTheme->insertColumn(newColumnIndex, mSelectedThemeColumn); mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = mDropIndicatorPoint2; setTheme(mTheme); // this will reset theme cache and trigger a global update } else { delete mSelectedThemeColumn; mSelectedThemeColumn = nullptr; } delete dlg; } void ThemePreviewWidget::slotColumnProperties() { if (!mSelectedThemeColumn) { return; } ThemeColumnPropertiesDialog *dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Column Properties")); if (dlg->exec() == QDialog::Accepted) { mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = mDropIndicatorPoint2; setTheme(mTheme); // this will reset theme cache and trigger a global update } delete dlg; } void ThemePreviewWidget::slotDeleteColumn() { if (!mSelectedThemeColumn) { return; } const int idx = mTheme->columns().indexOf(mSelectedThemeColumn); if (idx < 1) { // first column can't be deleted return; } mTheme->removeColumn(mSelectedThemeColumn); delete mSelectedThemeColumn; mSelectedThemeColumn = nullptr; mSelectedThemeContentItem = nullptr; mThemeSelectedContentItemRect = QRect(); mDropIndicatorPoint1 = mDropIndicatorPoint2; setTheme(mTheme); // this will reset theme cache and trigger a global update } ThemeEditor::ThemeEditor(QWidget *parent) : OptionSetEditor(parent) { mCurrentTheme = nullptr; // Appearance tab QWidget *tab = new QWidget(this); addTab(tab, i18n("Appearance")); QGridLayout *tabg = new QGridLayout(tab); QGroupBox *gb = new QGroupBox(i18n("Content Items"), tab); tabg->addWidget(gb, 0, 0); QGridLayout *gblayout = new QGridLayout(gb); Theme dummyTheme; ThemeContentItemSourceLabel *cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Subject); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 0); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Date); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 0); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Size); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 0); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Sender); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 1); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Receiver); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 1); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SenderOrReceiver); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 1); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::MostRecentDate); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 2); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::TagList); cil->setText(Theme::ContentItem::description(cil->type())); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 2); + + cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Folder); + cil->setText(Theme::ContentItem::description(cil->type())); + cil->setToolTip(Theme::ContentItem::description(cil->type())); + gblayout->addWidget(cil, 2, 2); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::CombinedReadRepliedStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconRepliedAndForwarded)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 3); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ReadStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconNew)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 3); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::RepliedStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconReplied)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 3); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::AttachmentStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconAttachment)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 4); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::EncryptionStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullyEncrypted)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 4); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SignatureStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullySigned)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 4); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ActionItemStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconActionItem)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 5); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::AnnotationIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconAnnotation)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 5); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::InvitationIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconInvitation)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 5); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ImportantStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconImportant)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 6); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SpamHamStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconSpam)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 6); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::WatchedIgnoredStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconWatched)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 6); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ExpandedStateIcon); cil->setPixmap(*dummyTheme.pixmap(Theme::IconShowMore)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 0, 7); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::VerticalLine); cil->setPixmap(*dummyTheme.pixmap(Theme::IconVerticalLine)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 1, 7); cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::HorizontalSpacer); cil->setPixmap(*dummyTheme.pixmap(Theme::IconHorizontalSpacer)); cil->setToolTip(Theme::ContentItem::description(cil->type())); gblayout->addWidget(cil, 2, 7); mPreviewWidget = new ThemePreviewWidget(tab); tabg->addWidget(mPreviewWidget, 1, 0); QLabel *l = new QLabel(tab); l->setText(i18n( "Right click on the header to add or modify columns. Drag the content items and drop them on the columns in order to compose your theme. Right click on the items inside the view for more options.")); l->setWordWrap(true); l->setAlignment(Qt::AlignCenter); tabg->addWidget(l, 2, 0); tabg->setRowStretch(1, 1); // Advanced tab tab = new QWidget(this); addTab(tab, i18nc("@title:tab Advanced theme settings", "Advanced")); tabg = new QGridLayout(tab); l = new QLabel(i18n("Header:"), tab); tabg->addWidget(l, 0, 0); mViewHeaderPolicyCombo = new QComboBox(tab); tabg->addWidget(mViewHeaderPolicyCombo, 0, 1); l = new QLabel(i18n("Icon size:"), tab); tabg->addWidget(l, 1, 0); mIconSizeSpinBox = new KPluralHandlingSpinBox(tab); mIconSizeSpinBox->setMinimum(8); mIconSizeSpinBox->setMaximum(64); mIconSizeSpinBox->setSuffix(ki18ncp("suffix in a spinbox", " pixel", " pixels")); QObject::connect(mIconSizeSpinBox, qOverload(&KPluralHandlingSpinBox::valueChanged), this, &ThemeEditor::slotIconSizeSpinBoxValueChanged); tabg->addWidget(mIconSizeSpinBox, 1, 1); tabg->setColumnStretch(1, 1); tabg->setRowStretch(2, 1); fillViewHeaderPolicyCombo(); } ThemeEditor::~ThemeEditor() { } void ThemeEditor::editTheme(Theme *set) { mCurrentTheme = set; mPreviewWidget->setTheme(mCurrentTheme); if (!mCurrentTheme) { setEnabled(false); return; } setEnabled(true); nameEdit()->setText(set->name()); descriptionEdit()->setPlainText(set->description()); ComboBoxUtils::setIntegerOptionComboValue(mViewHeaderPolicyCombo, (int)mCurrentTheme->viewHeaderPolicy()); mIconSizeSpinBox->setValue(set->iconSize()); setReadOnly(mCurrentTheme->readOnly()); } void ThemeEditor::setReadOnly(bool readOnly) { mPreviewWidget->setReadOnly(readOnly); mViewHeaderPolicyCombo->setEnabled(!readOnly); mIconSizeSpinBox->setEnabled(!readOnly); OptionSetEditor::setReadOnly(readOnly); } void ThemeEditor::commit() { if (!mCurrentTheme || mCurrentTheme->readOnly()) { return; } mCurrentTheme->setName(nameEdit()->text()); mCurrentTheme->setDescription(descriptionEdit()->toPlainText()); mCurrentTheme->setViewHeaderPolicy( (Theme::ViewHeaderPolicy)ComboBoxUtils::getIntegerOptionComboValue(mViewHeaderPolicyCombo, 0) ); mCurrentTheme->setIconSize(mIconSizeSpinBox->value()); // other settings are already committed to this theme } void ThemeEditor::fillViewHeaderPolicyCombo() { ComboBoxUtils::fillIntegerOptionCombo( mViewHeaderPolicyCombo, Theme::enumerateViewHeaderPolicyOptions() ); } void ThemeEditor::slotNameEditTextEdited(const QString &newName) { if (!mCurrentTheme) { return; } mCurrentTheme->setName(newName); Q_EMIT themeNameChanged(); } void ThemeEditor::slotIconSizeSpinBoxValueChanged(int val) { if (!mCurrentTheme) { return; } mCurrentTheme->setIconSize(val); mPreviewWidget->setTheme(mCurrentTheme); // will trigger a cache reset and a view update } MessageList::Core::Theme *ThemeEditor::editedTheme() const { return mCurrentTheme; }