diff --git a/src/todo/incidencetreemodel.cpp b/src/todo/incidencetreemodel.cpp index b871fb1..d9a90d5 100644 --- a/src/todo/incidencetreemodel.cpp +++ b/src/todo/incidencetreemodel.cpp @@ -1,919 +1,925 @@ /* Copyright (c) 2012 Sérgio Martins 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "incidencetreemodel_p.h" #include "calendarview_debug.h" #include using namespace Akonadi; QDebug operator<<(QDebug s, const Node::Ptr &node); static void calculateDepth(const Node::Ptr &node) { Q_ASSERT(node); node->depth = node->parentNode ? 1 + node->parentNode->depth : 0; foreach (const Node::Ptr &child, node->directChilds) { calculateDepth(child); } } // Desired ordering [...],3,2,1,0,-1 static bool reverseDepthLessThan(const Node::Ptr &node1, const Node::Ptr &node2) { return node1->depth > node2->depth; } // Desired ordering 0,1,2,3,[...],-1 static bool depthLessThan(const PreNode::Ptr &node1, const PreNode::Ptr &node2) { if (node1->depth == -1) { return false; } return node1->depth < node2->depth || node2->depth == -1; } static PreNode::List sortedPrenodes(const PreNode::List &nodes) { const int count = nodes.count(); QHash prenodeByUid; PreNode::List remainingNodes = nodes; while (prenodeByUid.count() < count) { bool foundAtLeastOne = false; // this bool saves us from infinit looping if the parent doesn't exist foreach (const PreNode::Ptr &node, remainingNodes) { Q_ASSERT(node); const QString uid = node->incidence->instanceIdentifier(); const QString parentUid = node->incidence->relatedTo(); if (parentUid.isEmpty()) { // toplevel todo prenodeByUid.insert(uid, node); remainingNodes.removeAll(node); node->depth = 0; foundAtLeastOne = true; } else { if (prenodeByUid.contains(parentUid)) { node->depth = 1 + prenodeByUid.value(parentUid)->depth; remainingNodes.removeAll(node); prenodeByUid.insert(uid, node); foundAtLeastOne = true; } } } if (!foundAtLeastOne) { break; } } PreNode::List sorted = nodes; std::sort(sorted.begin(), sorted.end(), depthLessThan); return sorted; } IncidenceTreeModel::Private::Private(IncidenceTreeModel *qq, const QStringList &mimeTypes) : QObject() , m_mimeTypes(mimeTypes) , q(qq) { } int IncidenceTreeModel::Private::rowForNode(const Node::Ptr &node) const { // Returns it's row number const int row = node->parentNode ? node->parentNode->directChilds.indexOf(node) : m_toplevelNodeList.indexOf(node); Q_ASSERT(row != -1); return row; } void IncidenceTreeModel::Private::assert_and_dump(bool condition, const QString &message) { if (!condition) { qCCritical(CALENDARVIEW_LOG) << "This should never happen: " << message; dumpTree(); Q_ASSERT(false); } } void IncidenceTreeModel::Private::dumpTree() { for (const Node::Ptr &node : qAsConst(m_toplevelNodeList)) { qCDebug(CALENDARVIEW_LOG) << node; } } QModelIndex IncidenceTreeModel::Private::indexForNode(const Node::Ptr &node) const { if (!node) { return QModelIndex(); } const int row = node->parentNode ? node->parentNode->directChilds.indexOf(node) : m_toplevelNodeList.indexOf(node); Q_ASSERT(row != -1); return q->createIndex(row, 0, node.data()); } void IncidenceTreeModel::Private::reset(bool silent) { if (!silent) { q->beginResetModel(); } m_toplevelNodeList.clear(); m_nodeMap.clear(); m_itemByUid.clear(); m_waitingForParent.clear(); m_uidMap.clear(); if (q->sourceModel()) { const int sourceCount = q->sourceModel()->rowCount(); for (int i = 0; i < sourceCount; ++i) { PreNode::Ptr prenode = prenodeFromSourceRow(i); if (prenode && (m_mimeTypes.isEmpty() || m_mimeTypes.contains(prenode->incidence->mimeType()))) { insertNode(prenode, /**silent=*/ true); } } } if (!silent) { q->endResetModel(); } } void IncidenceTreeModel::Private::onHeaderDataChanged(Qt::Orientation orientation, int first, int last) { Q_EMIT q->headerDataChanged(orientation, first, last); } void IncidenceTreeModel::Private::onDataChanged(const QModelIndex &begin, const QModelIndex &end) { Q_ASSERT(begin.isValid()); Q_ASSERT(end.isValid()); Q_ASSERT(q->sourceModel()); Q_ASSERT(!begin.parent().isValid()); Q_ASSERT(!end.parent().isValid()); Q_ASSERT(begin.row() <= end.row()); const int first_row = begin.row(); const int last_row = end.row(); for (int i = first_row; i <= last_row; ++i) { QModelIndex sourceIndex = q->sourceModel()->index(i, 0); Q_ASSERT(sourceIndex.isValid()); QModelIndex index = q->mapFromSource(sourceIndex); // Index might be invalid if we filter by incidence type. if (index.isValid()) { Q_ASSERT(index.internalPointer()); // Did we this node change parent? If no, just Q_EMIT dataChanged(), if // yes, we must Q_EMIT rowsMoved(), so we see a visual effect in the view. Node *rawNode = reinterpret_cast(index.internalPointer()); Node::Ptr node = m_uidMap.value(rawNode->uid); // Looks hackish but it's safe Q_ASSERT(node); Node::Ptr oldParentNode = node->parentNode; Akonadi::Item item = q->data(index, Akonadi::EntityTreeModel::ItemRole).value(); Q_ASSERT(item.isValid()); KCalCore::Incidence::Ptr incidence = !item.hasPayload() ? KCalCore::Incidence::Ptr() : item.payload(); if (!incidence) { qCCritical(CALENDARVIEW_LOG) << "Incidence shouldn't be invalid." << item.hasPayload() << item.id(); Q_ASSERT(false); return; } m_itemByUid.insert(incidence->instanceIdentifier(), item); Node::Ptr newParentNode; const QString newParentUid = incidence->relatedTo(); if (!newParentUid.isEmpty()) { Q_ASSERT(m_uidMap.contains(newParentUid)); newParentNode = m_uidMap.value(newParentUid); Q_ASSERT(newParentNode); } const bool parentChanged = newParentNode.data() != oldParentNode.data(); if (parentChanged) { const int fromRow = rowForNode(node); int toRow = -1; QModelIndex newParentIndex; // Calculate parameters for beginMoveRows() if (newParentNode) { newParentIndex = q->mapFromSource(newParentNode->sourceIndex); Q_ASSERT(newParentIndex.isValid()); toRow = newParentNode->directChilds.count(); } else { // New parent is 0, it's son of root now newParentIndex = QModelIndex(); toRow = m_toplevelNodeList.count(); } const bool res = q->beginMoveRows(/**fromParent*/ index.parent(), fromRow, fromRow, newParentIndex, toRow); Q_ASSERT(res); Q_UNUSED(res); // Now that beginmoveRows() was called, we can do the actual moving: if (newParentNode) { newParentNode->directChilds.append(node); // Add to new parent node->parentNode = newParentNode; if (oldParentNode) { oldParentNode->directChilds.remove(fromRow); // Remove from parent Q_ASSERT(oldParentNode->directChilds.indexOf(node) == -1); } else { m_toplevelNodeList.remove(fromRow); // Remove from root Q_ASSERT(m_toplevelNodeList.indexOf(node) == -1); } } else { // New parent is 0, it's son of root now m_toplevelNodeList.append(node); node->parentNode = Node::Ptr(); oldParentNode->directChilds.remove(fromRow); Q_ASSERT(oldParentNode->directChilds.indexOf(node) == -1); } q->endMoveRows(); // index is rotten after the move, retrieve it again index = indexForNode(node); Q_ASSERT(index.isValid()); if (newParentNode) { Q_EMIT q->indexChangedParent(index.parent()); } } else { Q_EMIT q->dataChanged(index, index); } } } } void IncidenceTreeModel::Private::onRowsAboutToBeInserted(const QModelIndex &parent, int, int) { // We are a reparenting proxy, the source proxy is flat Q_ASSERT(!parent.isValid()); Q_UNUSED(parent); // Nothing to do yet. We don't know if all the new incidences in this range belong to the same // parent yet. } PreNode::Ptr IncidenceTreeModel::Private::prenodeFromSourceRow(int row) const { PreNode::Ptr node = PreNode::Ptr(new PreNode()); node->sourceIndex = q->sourceModel()->index(row, 0, QModelIndex()); Q_ASSERT(node->sourceIndex.isValid()); Q_ASSERT(node->sourceIndex.model() == q->sourceModel()); const Akonadi::Item item = node->sourceIndex.data(EntityTreeModel::ItemRole).value(); if (!item.isValid()) { // It's a Collection, ignore that, we only want items. return PreNode::Ptr(); } node->item = item; node->incidence = item.payload(); Q_ASSERT(node->incidence); return node; } void IncidenceTreeModel::Private::onRowsInserted(const QModelIndex &parent, int begin, int end) { //QElapsedTimer timer; //timer.start(); Q_ASSERT(!parent.isValid()); Q_UNUSED(parent); Q_ASSERT(begin <= end); PreNode::List nodes; for (int i = begin; i <= end; ++i) { PreNode::Ptr node = prenodeFromSourceRow(i); // if m_mimeTypes is empty, we ignore this feature if (!node || (!m_mimeTypes.isEmpty() && !m_mimeTypes.contains(node->incidence->mimeType()))) { continue; } nodes << node; } PreNode::List sortedNodes = sortedPrenodes(nodes); foreach (const PreNode::Ptr &node, sortedNodes) { insertNode(node); } // view can now call KConfigViewStateSaver::restoreState(), to expand nodes. if (end > begin) { Q_EMIT q->batchInsertionFinished(); } //qCDebug(CALENDARVIEW_LOG) << "Took " << timer.elapsed() << " to insert " << end-begin+1; } void IncidenceTreeModel::Private::insertNode(const PreNode::Ptr &prenode, bool silent) { KCalCore::Incidence::Ptr incidence = prenode->incidence; Akonadi::Item item = prenode->item; Node::Ptr node(new Node()); node->sourceIndex = prenode->sourceIndex; node->id = item.id(); node->uid = incidence->instanceIdentifier(); m_itemByUid.insert(node->uid, item); //qCDebug(CALENDARVIEW_LOG) << "New node " << node.data() << node->uid << node->id; node->parentUid = incidence->relatedTo(); if (node->uid == node->parentUid) { qCWarning(CALENDARVIEW_LOG) << "Incidence with itself as parent!" << node->uid << "Akonadi item" << item.id() << "remoteId=" << item.remoteId(); node->parentUid.clear(); } if (m_uidMap.contains(node->uid)) { qCWarning(CALENDARVIEW_LOG) << "Duplicate incidence detected. File a bug against the resource. collection=" << item.storageCollectionId(); return; } Q_ASSERT(!m_nodeMap.contains(node->id)); m_uidMap.insert(node->uid, node); m_nodeMap.insert(item.id(), node); int rowToUse = -1; bool mustInsertIntoParent = false; const bool hasParent = !node->parentUid.isEmpty(); if (hasParent) { // We have a parent, did he arrive yet ? if (m_uidMap.contains(node->parentUid)) { node->parentNode = m_uidMap.value(node->parentUid); // We can only insert after beginInsertRows(), because it affects rowCounts mustInsertIntoParent = true; rowToUse = node->parentNode->directChilds.count(); } else { // Parent unknown, we are orphan for now Q_ASSERT(!m_waitingForParent.contains(node->parentUid, node)); m_waitingForParent.insert(node->parentUid, node); } } if (!node->parentNode) { rowToUse = m_toplevelNodeList.count(); } // Lets insert the row: const QModelIndex &parent = indexForNode(node->parentNode); if (!silent) { q->beginInsertRows(parent, rowToUse, rowToUse); } if (!node->parentNode) { m_toplevelNodeList.append(node); } if (mustInsertIntoParent) { node->parentNode->directChilds.append(node); } if (!silent) { q->endInsertRows(); } // Are we a parent? if (m_waitingForParent.contains(node->uid)) { Q_ASSERT(m_waitingForParent.count(node->uid) > 0); QList childs = m_waitingForParent.values(node->uid); m_waitingForParent.remove(node->uid); Q_ASSERT(!childs.isEmpty()); foreach (const Node::Ptr &child, childs) { const int fromRow = m_toplevelNodeList.indexOf(child); Q_ASSERT(fromRow != -1); const QModelIndex toParent = indexForNode(node); Q_ASSERT(toParent.isValid()); Q_ASSERT(toParent.model() == q); //const int toRow = node->directChilds.count(); if (!silent) { //const bool res = q->beginMoveRows( /**fromParent*/QModelIndex(), fromRow, // fromRow, toParent, toRow ); //Q_EMIT q->layoutAboutToBeChanged(); q->beginResetModel(); //Q_ASSERT( res ); } child->parentNode = node; node->directChilds.append(child); m_toplevelNodeList.remove(fromRow); if (!silent) { //q->endMoveRows(); q->endResetModel(); //Q_EMIT q->layoutChanged(); } } } } // Sorts childs first parents last Node::List IncidenceTreeModel::Private::sorted(const Node::List &nodes) const { if (nodes.isEmpty()) { return nodes; } // Initialize depths foreach (const Node::Ptr &topLevelNode, m_toplevelNodeList) { calculateDepth(topLevelNode); } Node::List sorted = nodes; std::sort(sorted.begin(), sorted.end(), reverseDepthLessThan); return sorted; } void IncidenceTreeModel::Private::onRowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end) { //QElapsedTimer timer; //timer.start(); Q_ASSERT(!parent.isValid()); Q_UNUSED(parent); Q_ASSERT(begin <= end); // First, gather nodes to remove Node::List nodesToRemove; for (int i = begin; i <= end; ++i) { QModelIndex sourceIndex = q->sourceModel()->index(i, 0, QModelIndex()); Q_ASSERT(sourceIndex.isValid()); Q_ASSERT(sourceIndex.model() == q->sourceModel()); const Akonadi::Item::Id id = sourceIndex.data(EntityTreeModel::ItemIdRole).toLongLong(); Q_ASSERT(id != -1); if (!m_nodeMap.contains(id)) { // We don't know about this one because we're ignoring it's mime type. Q_ASSERT(m_mimeTypes.count() != 3); continue; } Node::Ptr node = m_nodeMap.value(id); Q_ASSERT(node->id == id); nodesToRemove << node; } // We want to remove childs first, to avoid row moving Node::List nodesToRemoveSorted = sorted(nodesToRemove); foreach (const Node::Ptr &node, nodesToRemoveSorted) { // Go ahead and remove it now. We don't do it in ::onRowsRemoved(), because // while unparenting childs with moveRows() the view might call data() on the // item that is already removed from ETM. removeNode(node); //qCDebug(CALENDARVIEW_LOG) << "Just removed a node, here's the tree"; //dumpTree(); } m_removedNodes.clear(); //qCDebug(CALENDARVIEW_LOG) << "Took " << timer.elapsed() << " to remove " << end-begin+1; } void IncidenceTreeModel::Private::removeNode(const Node::Ptr &node) { Q_ASSERT(node); //qCDebug(CALENDARVIEW_LOG) << "Dealing with parent: " << node->id << node.data() // << node->uid << node->directChilds.count() << indexForNode( node ); // First, unparent the children if (!node->directChilds.isEmpty()) { Node::List childs = node->directChilds; const QModelIndex fromParent = indexForNode(node); Q_ASSERT(fromParent.isValid()); // const int firstSourceRow = 0; // const int lastSourceRow = node->directChilds.count() - 1; //const int toRow = m_toplevelNodeList.count(); //q->beginMoveRows( fromParent, firstSourceRow, lastSourceRow, // /**toParent is root*/QModelIndex(), toRow ); q->beginResetModel(); node->directChilds.clear(); foreach (const Node::Ptr &child, childs) { //qCDebug(CALENDARVIEW_LOG) << "Dealing with child: " << child.data() << child->uid; m_toplevelNodeList.append(child); child->parentNode = Node::Ptr(); m_waitingForParent.insert(node->uid, child); } //q->endMoveRows(); q->endResetModel(); } const QModelIndex parent = indexForNode(node->parentNode); const int rowToRemove = rowForNode(node); // Now remove the row Q_ASSERT(!(parent.isValid() && parent.model() != q)); q->beginRemoveRows(parent, rowToRemove, rowToRemove); m_itemByUid.remove(node->uid); if (parent.isValid()) { node->parentNode->directChilds.remove(rowToRemove); node->parentNode = Node::Ptr(); } else { m_toplevelNodeList.remove(rowToRemove); } if (!node->parentUid.isEmpty()) { m_waitingForParent.remove(node->parentUid, node); } m_uidMap.remove(node->uid); m_nodeMap.remove(node->id); q->endRemoveRows(); m_removedNodes << node.data(); } void IncidenceTreeModel::Private::onRowsRemoved(const QModelIndex &parent, int begin, int end) { Q_UNUSED(parent); Q_UNUSED(begin); Q_UNUSED(end); // Nothing to do here, see comment on ::onRowsAboutToBeRemoved() } void IncidenceTreeModel::Private::onModelAboutToBeReset() { q->beginResetModel(); } void IncidenceTreeModel::Private::onModelReset() { reset(/**silent=*/ false); q->endResetModel(); } void IncidenceTreeModel::Private::onLayoutAboutToBeChanged() { Q_ASSERT(q->persistentIndexList().isEmpty()); Q_EMIT q->layoutAboutToBeChanged(); } void IncidenceTreeModel::Private::onLayoutChanged() { reset(/**silent=*/ true); Q_ASSERT(q->persistentIndexList().isEmpty()); Q_EMIT q->layoutChanged(); } void IncidenceTreeModel::Private::onRowsMoved(const QModelIndex &, int, int, const QModelIndex &, int) { // Not implemented yet Q_ASSERT(false); } +void IncidenceTreeModel::Private::setSourceModel(QAbstractItemModel *model) +{ + q->beginResetModel(); + + if (q->sourceModel()) { + disconnect(q->sourceModel(), &IncidenceTreeModel::dataChanged, + this, &IncidenceTreeModel::Private::onDataChanged); + + disconnect(q->sourceModel(), &IncidenceTreeModel::headerDataChanged, + this, &IncidenceTreeModel::Private::onHeaderDataChanged); + + disconnect(q->sourceModel(), &IncidenceTreeModel::rowsInserted, + this, &IncidenceTreeModel::Private::onRowsInserted); + + disconnect(q->sourceModel(), &IncidenceTreeModel::rowsRemoved, + this, &IncidenceTreeModel::Private::onRowsRemoved); + + disconnect(q->sourceModel(), &IncidenceTreeModel::rowsMoved, + this, &IncidenceTreeModel::Private::onRowsMoved); + + disconnect(q->sourceModel(), &IncidenceTreeModel::rowsAboutToBeInserted, + this, &IncidenceTreeModel::Private::onRowsAboutToBeInserted); + + disconnect(q->sourceModel(), &IncidenceTreeModel::rowsAboutToBeRemoved, + this, &IncidenceTreeModel::Private::onRowsAboutToBeRemoved); + + disconnect(q->sourceModel(), &IncidenceTreeModel::modelAboutToBeReset, + this, &IncidenceTreeModel::Private::onModelAboutToBeReset); + + disconnect(q->sourceModel(), &IncidenceTreeModel::modelReset, + this, &IncidenceTreeModel::Private::onModelReset); + + disconnect(q->sourceModel(), &IncidenceTreeModel::layoutAboutToBeChanged, + this, &IncidenceTreeModel::Private::onLayoutAboutToBeChanged); + + disconnect(q->sourceModel(), &IncidenceTreeModel::layoutChanged, + this, &IncidenceTreeModel::Private::onLayoutChanged); + } + + q->QAbstractProxyModel::setSourceModel(model); + + if (q->sourceModel()) { + connect(q->sourceModel(), &IncidenceTreeModel::dataChanged, + this, &IncidenceTreeModel::Private::onDataChanged); + + connect(q->sourceModel(), &IncidenceTreeModel::headerDataChanged, + this, &IncidenceTreeModel::Private::onHeaderDataChanged); + + connect(q->sourceModel(), &IncidenceTreeModel::rowsAboutToBeInserted, + this, &IncidenceTreeModel::Private::onRowsAboutToBeInserted); + + connect(q->sourceModel(), &IncidenceTreeModel::rowsInserted, + this, &IncidenceTreeModel::Private::onRowsInserted); + + connect(q->sourceModel(), &IncidenceTreeModel::rowsAboutToBeRemoved, + this, &IncidenceTreeModel::Private::onRowsAboutToBeRemoved); + + connect(q->sourceModel(), &IncidenceTreeModel::rowsRemoved, + this, &IncidenceTreeModel::Private::onRowsRemoved); + + connect(q->sourceModel(), &IncidenceTreeModel::rowsMoved, + this, &IncidenceTreeModel::Private::onRowsMoved); + + connect(q->sourceModel(), &IncidenceTreeModel::modelAboutToBeReset, + this, &IncidenceTreeModel::Private::onModelAboutToBeReset); + + connect(q->sourceModel(), &IncidenceTreeModel::modelReset, + this, &IncidenceTreeModel::Private::onModelReset); + + connect(q->sourceModel(), &IncidenceTreeModel::layoutAboutToBeChanged, + this, &IncidenceTreeModel::Private::onLayoutAboutToBeChanged); + + connect(q->sourceModel(), &IncidenceTreeModel::layoutChanged, + this, &IncidenceTreeModel::Private::onLayoutChanged); + } + + reset(/**silent=*/ true); + q->endResetModel(); +} + + + IncidenceTreeModel::IncidenceTreeModel(QObject *parent) : QAbstractProxyModel(parent) , d(new Private(this, QStringList())) { setObjectName(QStringLiteral("IncidenceTreeModel")); } IncidenceTreeModel::IncidenceTreeModel(const QStringList &mimeTypes, QObject *parent) : QAbstractProxyModel(parent) , d(new Private(this, mimeTypes)) { setObjectName(QStringLiteral("IncidenceTreeModel")); } IncidenceTreeModel::~IncidenceTreeModel() { delete d; } QVariant IncidenceTreeModel::data(const QModelIndex &index, int role) const { Q_ASSERT(index.isValid()); if (!index.isValid() || !sourceModel()) { return QVariant(); } QModelIndex sourceIndex = mapToSource(index); Q_ASSERT(sourceIndex.isValid()); return sourceModel()->data(sourceIndex, role); } int IncidenceTreeModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { Q_ASSERT(parent.model() == this); Node *parentNode = reinterpret_cast(parent.internalPointer()); Q_ASSERT(parentNode); d->assert_and_dump(!d->m_removedNodes.contains(parentNode), QString::number((quintptr)parentNode, 16) + QLatin1String(" was already deleted")); const int count = parentNode->directChilds.count(); return count; } return d->m_toplevelNodeList.count(); } int IncidenceTreeModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { Q_ASSERT(parent.model() == this); } return sourceModel() ? sourceModel()->columnCount() : 1; } void IncidenceTreeModel::setSourceModel(QAbstractItemModel *model) { if (model == sourceModel()) { return; } - - beginResetModel(); - - if (sourceModel()) { - disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), - d, SLOT(onDataChanged(QModelIndex,QModelIndex))); - - disconnect(sourceModel(), SIGNAL(headerDataChanged(Qt::Orientation,int,int)), - d, SLOT(onHeaderDataChanged(Qt::Orientation,int,int))); - - disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), - d, SLOT(onRowsInserted(QModelIndex,int,int))); - - disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), - d, SLOT(onRowsRemoved(QModelIndex,int,int))); - - disconnect(sourceModel(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), - d, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int))); - - disconnect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), - d, SLOT(onRowsAboutToBeInserted(QModelIndex,int,int))); - - disconnect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), - d, SLOT(onRowsAboutToBeRemoved(QModelIndex,int,int))); - - disconnect(sourceModel(), SIGNAL(modelAboutToBeReset()), - d, SLOT(onModelAboutToBeReset())); - - disconnect(sourceModel(), SIGNAL(modelReset()), - d, SLOT(onModelReset())); - - disconnect(sourceModel(), SIGNAL(layoutAboutToBeChanged()), - d, SLOT(onLayoutAboutToBeChanged())); - - disconnect(sourceModel(), SIGNAL(layoutChanged()), - d, SLOT(onLayoutChanged())); - } - - QAbstractProxyModel::setSourceModel(model); - - if (sourceModel()) { - connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), - d, SLOT(onDataChanged(QModelIndex,QModelIndex))); - - connect(sourceModel(), SIGNAL(headerDataChanged(Qt::Orientation,int,int)), - d, SLOT(onHeaderDataChanged(Qt::Orientation,int,int))); - - connect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), - d, SLOT(onRowsAboutToBeInserted(QModelIndex,int,int))); - - connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), - d, SLOT(onRowsInserted(QModelIndex,int,int))); - - connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), - d, SLOT(onRowsAboutToBeRemoved(QModelIndex,int,int))); - - connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), - d, SLOT(onRowsRemoved(QModelIndex,int,int))); - - connect(sourceModel(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), - d, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int))); - - connect(sourceModel(), SIGNAL(modelAboutToBeReset()), - d, SLOT(onModelAboutToBeReset())); - - connect(sourceModel(), SIGNAL(modelReset()), - d, SLOT(onModelReset())); - - connect(sourceModel(), SIGNAL(layoutAboutToBeChanged()), - d, SLOT(onLayoutAboutToBeChanged())); - - connect(sourceModel(), SIGNAL(layoutChanged()), - d, SLOT(onLayoutChanged())); - } - - d->reset(/**silent=*/ true); - endResetModel(); + d->setSourceModel(model); } QModelIndex IncidenceTreeModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) { qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::mapFromSource() source index is invalid"; // Q_ASSERT( false ); return QModelIndex(); } if (!sourceModel()) { return QModelIndex(); } Q_ASSERT(sourceIndex.column() < sourceModel()->columnCount()); Q_ASSERT(sourceModel() == sourceIndex.model()); const Akonadi::Item::Id id = sourceIndex.data(Akonadi::EntityTreeModel::ItemIdRole).toLongLong(); if (id == -1 || !d->m_nodeMap.contains(id)) { return QModelIndex(); } const Node::Ptr node = d->m_nodeMap.value(id); Q_ASSERT(node); return d->indexForNode(node); } QModelIndex IncidenceTreeModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid() || !sourceModel()) { return QModelIndex(); } Q_ASSERT(proxyIndex.column() < columnCount()); Q_ASSERT(proxyIndex.internalPointer()); Q_ASSERT(proxyIndex.model() == this); Node *node = reinterpret_cast(proxyIndex.internalPointer()); /* This code is slow, using a persistent model index instead. QModelIndexList indexes = EntityTreeModel::modelIndexesForItem( sourceModel(), Akonadi::Item( node->id ) ); if ( indexes.isEmpty() ) { Q_ASSERT( sourceModel() ); qCCritical(CALENDARVIEW_LOG) << "IncidenceTreeModel::mapToSource() no indexes." << proxyIndex << node->id << "; source.rowCount() = " << sourceModel()->rowCount() << "; source=" << sourceModel() << "rowCount()" << rowCount(); Q_ASSERT( false ); return QModelIndex(); } QModelIndex index = indexes.first();*/ QModelIndex index = node->sourceIndex; if (!index.isValid()) { qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::mapToSource(): sourceModelIndex is invalid"; Q_ASSERT(false); return QModelIndex(); } Q_ASSERT(index.model() == sourceModel()); return index.sibling(index.row(), proxyIndex.column()); } QModelIndex IncidenceTreeModel::parent(const QModelIndex &child) const { if (!child.isValid()) { qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::parent(): child is invalid"; Q_ASSERT(false); return QModelIndex(); } Q_ASSERT(child.model() == this); Q_ASSERT(child.internalPointer()); Node *childNode = reinterpret_cast(child.internalPointer()); if (d->m_removedNodes.contains(childNode)) { qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::parent() Node already removed."; return QModelIndex(); } if (!childNode->parentNode) { return QModelIndex(); } const QModelIndex parentIndex = d->indexForNode(childNode->parentNode); if (!parentIndex.isValid()) { qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::parent(): proxyModelIndex is invalid."; Q_ASSERT(false); return QModelIndex(); } Q_ASSERT(parentIndex.model() == this); Q_ASSERT(childNode->parentNode.data()); // Parent is always at row 0 return parentIndex; } QModelIndex IncidenceTreeModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || row >= rowCount(parent)) { // This is ok apparently /*qCWarning(CALENDARVIEW_LOG) << "IncidenceTreeModel::index() parent.isValid()" << parent.isValid() << "; row=" << row << "; column=" << column << "; rowCount() = " << rowCount( parent ); */ // Q_ASSERT( false ); return QModelIndex(); } Q_ASSERT(column >= 0); Q_ASSERT(column < columnCount()); if (parent.isValid()) { Q_ASSERT(parent.model() == this); Q_ASSERT(parent.internalPointer()); Node *parentNode = reinterpret_cast(parent.internalPointer()); if (row >= parentNode->directChilds.count()) { qCCritical(CALENDARVIEW_LOG) << "IncidenceTreeModel::index() row=" << row << "; column=" << column; Q_ASSERT(false); return QModelIndex(); } return createIndex(row, column, parentNode->directChilds.at(row).data()); } else { Q_ASSERT(row < d->m_toplevelNodeList.count()); Node::Ptr node = d->m_toplevelNodeList.at(row); Q_ASSERT(node); return createIndex(row, column, node.data()); } } bool IncidenceTreeModel::hasChildren(const QModelIndex &parent) const { if (parent.isValid()) { Q_ASSERT(parent.column() < columnCount()); if (parent.column() != 0) { // Indexes at column >0 don't have parent, says Qt documentation return false; } Node *parentNode = reinterpret_cast(parent.internalPointer()); Q_ASSERT(parentNode); return !parentNode->directChilds.isEmpty(); } else { return !d->m_toplevelNodeList.isEmpty(); } } Akonadi::Item IncidenceTreeModel::item(const QString &uid) const { Akonadi::Item item; if (uid.isEmpty()) { qCWarning(CALENDARVIEW_LOG) << "Called with an empty uid"; } else { if (d->m_itemByUid.contains(uid)) { item = d->m_itemByUid.value(uid); } else { qCWarning(CALENDARVIEW_LOG) << "There's no incidence with uid " << uid; } } return item; } QDebug operator<<(QDebug s, const Node::Ptr &node) { Q_ASSERT(node); static int level = 0; ++level; QString padding = QString(level - 1, QLatin1Char(' ')); s << padding + QLatin1String("node") << node.data() << QStringLiteral(";uid=") << node->uid << QStringLiteral(";id=") << node->id << QStringLiteral(";parentUid=") << node->parentUid << QStringLiteral(";parentNode=") << (void *)(node->parentNode.data()) << '\n'; foreach (const Node::Ptr &child, node->directChilds) { s << child; } --level; return s; } diff --git a/src/todo/incidencetreemodel_p.h b/src/todo/incidencetreemodel_p.h index c073e05..b275eab 100644 --- a/src/todo/incidencetreemodel_p.h +++ b/src/todo/incidencetreemodel_p.h @@ -1,113 +1,114 @@ /* Copyright (c) 2012 Sérgio Martins 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #ifndef INCIDENCE_TREEMODEL_P_H #define INCIDENCE_TREEMODEL_P_H #include "incidencetreemodel.h" #include #include #include #include #include #include #include #include #include typedef QString Uid; typedef QString ParentUid; struct Node { typedef QSharedPointer Ptr; typedef QMap Map; typedef QVector List; QPersistentModelIndex sourceIndex; // because ETM::modelIndexesForItem is so slow Akonadi::Item::Id id; Node::Ptr parentNode; QString parentUid; QString uid; List directChilds; int depth; }; /** Just a struct to contain some data before we create the node */ struct PreNode { typedef QSharedPointer Ptr; typedef QList List; KCalCore::Incidence::Ptr incidence; QPersistentModelIndex sourceIndex; Akonadi::Item item; int depth; PreNode() : depth(-1) { } }; class IncidenceTreeModel::Private : public QObject { Q_OBJECT public: Private(IncidenceTreeModel *qq, const QStringList &mimeTypes); void reset(bool silent = false); void insertNode(const PreNode::Ptr &node, bool silent = false); void insertNode(const QModelIndex &sourceIndex, bool silent = false); void removeNode(const Node::Ptr &node); QModelIndex indexForNode(const Node::Ptr &node) const; int rowForNode(const Node::Ptr &node) const; bool indexBeingRemoved(const QModelIndex &) const; // Is it being removed? void dumpTree(); void assert_and_dump(bool condition, const QString &message); Node::List sorted(const Node::List &nodes) const; PreNode::Ptr prenodeFromSourceRow(int sourceRow) const; - + void setSourceModel(QAbstractItemModel *model); + public: Node::Map m_nodeMap; Node::List m_toplevelNodeList; QHash m_uidMap; QHash m_itemByUid; QMultiHash m_waitingForParent; QList m_removedNodes; const QStringList m_mimeTypes; private Q_SLOTS: void onHeaderDataChanged(Qt::Orientation orientation, int first, int last); void onDataChanged(const QModelIndex &begin, const QModelIndex &end); void onRowsAboutToBeInserted(const QModelIndex &parent, int begin, int end); void onRowsInserted(const QModelIndex &parent, int begin, int end); void onRowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end); void onRowsRemoved(const QModelIndex &parent, int begin, int end); void onRowsMoved(const QModelIndex &, int, int, const QModelIndex &, int); void onModelAboutToBeReset(); void onModelReset(); void onLayoutAboutToBeChanged(); void onLayoutChanged(); private: IncidenceTreeModel *q; }; #endif diff --git a/src/todo/todoview.cpp b/src/todo/todoview.cpp index 7d1a2b1..bab92ca 100644 --- a/src/todo/todoview.cpp +++ b/src/todo/todoview.cpp @@ -1,1283 +1,1283 @@ /* This file is part of KOrganizer. Copyright (c) 2000,2001,2003 Cornelius Schumacher Copyright (c) 2003-2004 Reinhold Kainhofer Copyright (c) 2005 Rafal Rzepecki Copyright (c) 2008 Thomas Thrainer Copyright (c) 2013 Sérgio Martins 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "todoview.h" #include "incidencetreemodel.h" #include "tododelegates.h" #include "todomodel.h" #include "todoviewquickaddline.h" #include "todoviewquicksearch.h" #include "todoviewsortfilterproxymodel.h" #include "todoviewview.h" #include "calendarview_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QPointer) using namespace EventViews; using namespace KCalCore; namespace EventViews { // We share this struct between all views, for performance and memory purposes class ModelStack { public: ModelStack(const EventViews::PrefsPtr &preferences, QObject *parent_) : todoModel(new TodoModel(preferences)) , parent(parent_) , calendar(nullptr) , todoTreeModel(nullptr) , todoFlatModel(nullptr) , prefs(preferences) { } ~ModelStack() { delete todoModel; delete todoTreeModel; delete todoFlatModel; } void registerView(TodoView *view) { views << view; } void unregisterView(TodoView *view) { views.removeAll(view); } void setFlatView(bool flat) { const QString todoMimeType = QStringLiteral("application/x-vnd.akonadi.calendar.todo"); if (flat) { foreach (TodoView *view, views) { // In flatview dropping confuses users and it's very easy to drop into a child item view->mView->setDragDropMode(QAbstractItemView::DragOnly); view->setFlatView(flat, /**propagate=*/ false); // So other views update their toggle icon if (todoTreeModel) { view->saveViewState(); // Save the tree state before it's gone } } delete todoFlatModel; todoFlatModel = new Akonadi::EntityMimeTypeFilterModel(parent); todoFlatModel->addMimeTypeInclusionFilter(todoMimeType); todoFlatModel->setSourceModel(calendar ? calendar->model() : nullptr); todoModel->setSourceModel(todoFlatModel); delete todoTreeModel; todoTreeModel = nullptr; } else { delete todoTreeModel; todoTreeModel = new IncidenceTreeModel(QStringList() << todoMimeType, parent); foreach (TodoView *view, views) { QObject::connect(todoTreeModel, &IncidenceTreeModel::indexChangedParent, view, &TodoView::expandIndex); QObject::connect(todoTreeModel, &IncidenceTreeModel::batchInsertionFinished, view, &TodoView::restoreViewState); view->mView->setDragDropMode(QAbstractItemView::DragDrop); view->setFlatView(flat, /**propagate=*/ false); // So other views update their toggle icon } todoTreeModel->setSourceModel(calendar ? calendar->model() : nullptr); todoModel->setSourceModel(todoTreeModel); delete todoFlatModel; todoFlatModel = nullptr; } foreach (TodoView *view, views) { view->mFlatViewButton->blockSignals(true); // We block signals to avoid recursion, we have two TodoViews and mFlatViewButton is synchronized view->mFlatViewButton->setChecked(flat); view->mFlatViewButton->blockSignals(false); view->mView->setRootIsDecorated(!flat); view->restoreViewState(); } prefs->setFlatListTodo(flat); prefs->writeConfig(); } void setCalendar(const Akonadi::ETMCalendar::Ptr &newCalendar) { calendar = newCalendar; todoModel->setCalendar(calendar); if (todoTreeModel) { todoTreeModel->setSourceModel(calendar ? calendar->model() : nullptr); } } bool isFlatView() const { return todoFlatModel != nullptr; } TodoModel *todoModel = nullptr; QList views; QObject *parent = nullptr; Akonadi::ETMCalendar::Ptr calendar; IncidenceTreeModel *todoTreeModel = nullptr; Akonadi::EntityMimeTypeFilterModel *todoFlatModel = nullptr; EventViews::PrefsPtr prefs; }; } // Don't use K_GLOBAL_STATIC, see QTBUG-22667 static ModelStack *sModels = nullptr; TodoView::TodoView(const EventViews::PrefsPtr &prefs, bool sidebarView, QWidget *parent) : EventView(parent) , mQuickSearch(nullptr) , mQuickAdd(nullptr) , mTreeStateRestorer(nullptr) , mSidebarView(sidebarView) , mResizeColumnsScheduled(false) { mResizeColumnsTimer = new QTimer(this); connect(mResizeColumnsTimer, &QTimer::timeout, this, &TodoView::resizeColumns); mResizeColumnsTimer->setInterval(100); // so we don't overdue it when user resizes window manually mResizeColumnsTimer->setSingleShot(true); setPreferences(prefs); if (!sModels) { sModels = new ModelStack(prefs, parent); } sModels->registerView(this); mProxyModel = new TodoViewSortFilterProxyModel(preferences(), this); mProxyModel->setSourceModel(sModels->todoModel); mProxyModel->setDynamicSortFilter(true); mProxyModel->setFilterKeyColumn(TodoModel::SummaryColumn); mProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); mProxyModel->setSortRole(Qt::EditRole); connect(mProxyModel, &TodoViewSortFilterProxyModel::rowsInserted, this, &TodoView::onRowsInserted); if (!mSidebarView) { mQuickSearch = new TodoViewQuickSearch(calendar(), this); mQuickSearch->setVisible(prefs->enableTodoQuickSearch()); - connect(mQuickSearch, SIGNAL(searchTextChanged(QString)), - mProxyModel, SLOT(setFilterRegExp(QString))); + connect(mQuickSearch, &TodoViewQuickSearch::searchTextChanged, + mProxyModel, QOverload::of(&QSortFilterProxyModel::setFilterRegExp)); connect(mQuickSearch, &TodoViewQuickSearch::searchTextChanged, this, &TodoView::restoreViewState); connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, mProxyModel, &TodoViewSortFilterProxyModel::setCategoryFilter); connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, this, &TodoView::restoreViewState); connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, mProxyModel, &TodoViewSortFilterProxyModel::setPriorityFilter); connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, this, &TodoView::restoreViewState); } mView = new TodoViewView(this); mView->setModel(mProxyModel); mView->setContextMenuPolicy(Qt::CustomContextMenu); mView->setSortingEnabled(true); mView->setAutoExpandDelay(250); mView->setDragDropMode(QAbstractItemView::DragDrop); mView->setExpandsOnDoubleClick(false); mView->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); connect(mView->header(), &QHeaderView::geometriesChanged, this, &TodoView::scheduleResizeColumns); connect(mView, &TodoViewView::visibleColumnCountChanged, this, &TodoView::resizeColumns); TodoRichTextDelegate *richTextDelegate = new TodoRichTextDelegate(mView); mView->setItemDelegateForColumn(TodoModel::SummaryColumn, richTextDelegate); mView->setItemDelegateForColumn(TodoModel::DescriptionColumn, richTextDelegate); TodoPriorityDelegate *priorityDelegate = new TodoPriorityDelegate(mView); mView->setItemDelegateForColumn(TodoModel::PriorityColumn, priorityDelegate); TodoDueDateDelegate *startDateDelegate = new TodoDueDateDelegate(mView); mView->setItemDelegateForColumn(TodoModel::StartDateColumn, startDateDelegate); TodoDueDateDelegate *dueDateDelegate = new TodoDueDateDelegate(mView); mView->setItemDelegateForColumn(TodoModel::DueDateColumn, dueDateDelegate); TodoCompleteDelegate *completeDelegate = new TodoCompleteDelegate(mView); mView->setItemDelegateForColumn(TodoModel::PercentColumn, completeDelegate); mCategoriesDelegate = new TodoCategoriesDelegate(mView); mView->setItemDelegateForColumn(TodoModel::CategoriesColumn, mCategoriesDelegate); connect(mView, &TodoViewView::customContextMenuRequested, this, &TodoView::contextMenu); connect(mView, &TodoViewView::doubleClicked, this, &TodoView::itemDoubleClicked); connect( mView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TodoView::selectionChanged); mQuickAdd = new TodoViewQuickAddLine(this); mQuickAdd->setClearButtonEnabled(true); mQuickAdd->setVisible(preferences()->enableQuickTodo()); connect(mQuickAdd, SIGNAL(returnPressed(Qt::KeyboardModifiers)), this, SLOT(addQuickTodo(Qt::KeyboardModifiers))); mFullViewButton = nullptr; if (!mSidebarView) { mFullViewButton = new QToolButton(this); mFullViewButton->setAutoRaise(true); mFullViewButton->setCheckable(true); mFullViewButton->setToolTip( i18nc("@info:tooltip", "Display to-do list in a full window")); mFullViewButton->setWhatsThis( i18nc("@info:whatsthis", "Checking this option will cause the to-do view to use the full window.")); } mFlatViewButton = new QToolButton(this); mFlatViewButton->setAutoRaise(true); mFlatViewButton->setCheckable(true); mFlatViewButton->setToolTip( i18nc("@info:tooltip", "Display to-dos in flat list instead of a tree")); mFlatViewButton->setWhatsThis( i18nc("@info:whatsthis", "Checking this option will cause the to-dos to be displayed as a " "flat list instead of a hierarchical tree; the parental " "relationships are removed in the display.")); connect(mFlatViewButton, SIGNAL(toggled(bool)), SLOT(setFlatView(bool))); if (mFullViewButton) { connect(mFullViewButton, &QToolButton::toggled, this, &TodoView::setFullView); } QGridLayout *layout = new QGridLayout(this); layout->setMargin(0); if (!mSidebarView) { layout->addWidget(mQuickSearch, 0, 0, 1, 2); } layout->addWidget(mView, 1, 0, 1, 2); layout->setRowStretch(1, 1); layout->addWidget(mQuickAdd, 2, 0); // Dummy layout just to add a few px of right margin so the checkbox is aligned // with the QAbstractItemView's viewport. QHBoxLayout *dummyLayout = new QHBoxLayout(); dummyLayout->setContentsMargins(0, 0, mView->frameWidth() /*right*/, 0); if (!mSidebarView) { QFrame *f = new QFrame(this); f->setFrameShape(QFrame::VLine); f->setFrameShadow(QFrame::Sunken); dummyLayout->addWidget(f); dummyLayout->addWidget(mFullViewButton); } dummyLayout->addWidget(mFlatViewButton); layout->addLayout(dummyLayout, 2, 1); // ---------------- POPUP-MENUS ----------------------- mItemPopupMenu = new QMenu(this); mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction( i18nc("@action:inmenu show the to-do", "&Show"), this, SLOT(showTodo())); QAction *a = mItemPopupMenu->addAction( i18nc("@action:inmenu edit the to-do", "&Edit..."), this, &TodoView::editTodo); mItemPopupMenuReadWriteEntries << a; mItemPopupMenuItemOnlyEntries << a; mItemPopupMenu->addSeparator(); mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("document-print")), i18nc("@action:inmenu print the to-do", "&Print..."), this, &TodoView::printTodo); mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("document-print-preview")), i18nc("@action:inmenu print preview the to-do", "Print Previe&w..."), this, SIGNAL(printPreviewTodo())); mItemPopupMenu->addSeparator(); a = mItemPopupMenu->addAction( KIconLoader::global()->loadIcon(QStringLiteral("edit-delete"), KIconLoader::NoGroup, KIconLoader::SizeSmall), i18nc("@action:inmenu delete the to-do", "&Delete"), this, SLOT(deleteTodo())); mItemPopupMenuReadWriteEntries << a; mItemPopupMenuItemOnlyEntries << a; mItemPopupMenu->addSeparator(); mItemPopupMenu->addAction( KIconLoader::global()->loadIcon( QStringLiteral("view-calendar-tasks"), KIconLoader::NoGroup, KIconLoader::SizeSmall), i18nc("@action:inmenu create a new to-do", "New &To-do..."), this, SLOT(newTodo())); a = mItemPopupMenu->addAction( i18nc("@action:inmenu create a new sub-to-do", "New Su&b-to-do..."), this, SLOT(newSubTodo())); mItemPopupMenuReadWriteEntries << a; mItemPopupMenuItemOnlyEntries << a; mMakeTodoIndependent = mItemPopupMenu->addAction( i18nc("@action:inmenu", "&Make this To-do Independent"), this, SIGNAL(unSubTodoSignal())); mMakeSubtodosIndependent = mItemPopupMenu->addAction( i18nc("@action:inmenu", "Make all Sub-to-dos &Independent"), this, SIGNAL(unAllSubTodoSignal())); mItemPopupMenuItemOnlyEntries << mMakeTodoIndependent; mItemPopupMenuItemOnlyEntries << mMakeSubtodosIndependent; mItemPopupMenuReadWriteEntries << mMakeTodoIndependent; mItemPopupMenuReadWriteEntries << mMakeSubtodosIndependent; mItemPopupMenu->addSeparator(); a = mItemPopupMenu->addAction(KIconLoader::global()->loadIcon(QStringLiteral("appointment-new"), KIconLoader::NoGroup, KIconLoader::SizeSmall), i18nc("@action:inmenu", "Create Event"), this, SLOT(createEvent())); a->setObjectName(QStringLiteral("createevent")); mItemPopupMenuReadWriteEntries << a; mItemPopupMenuItemOnlyEntries << a; a = mItemPopupMenu->addAction(KIconLoader::global()->loadIcon(QStringLiteral("view-pim-notes"), KIconLoader::NoGroup, KIconLoader::SizeSmall), i18nc("@action:inmenu", "Create Note"), this, SLOT(createNote())); a->setObjectName(QStringLiteral("createnote")); mItemPopupMenuReadWriteEntries << a; mItemPopupMenuItemOnlyEntries << a; mItemPopupMenu->addSeparator(); mCopyPopupMenu = new KPIM::KDatePickerPopup(KPIM::KDatePickerPopup::NoDate |KPIM::KDatePickerPopup::DatePicker |KPIM::KDatePickerPopup::Words, QDate::currentDate(), this); mCopyPopupMenu->setTitle(i18nc("@title:menu", "&Copy To")); connect(mCopyPopupMenu, &KPIM::KDatePickerPopup::dateChanged, this, &TodoView::copyTodoToDate); connect(mCopyPopupMenu, &KPIM::KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide); mMovePopupMenu = new KPIM:: KDatePickerPopup(KPIM::KDatePickerPopup::NoDate |KPIM::KDatePickerPopup::DatePicker |KPIM::KDatePickerPopup::Words, QDate::currentDate(), this); mMovePopupMenu->setTitle(i18nc("@title:menu", "&Move To")); connect(mMovePopupMenu, &KPIM::KDatePickerPopup::dateChanged, this, &TodoView::setNewDate); connect(mMovePopupMenu, &KPIM::KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide); mItemPopupMenu->insertMenu(nullptr, mCopyPopupMenu); mItemPopupMenu->insertMenu(nullptr, mMovePopupMenu); mItemPopupMenu->addSeparator(); mItemPopupMenu->addAction( i18nc("@action:inmenu delete completed to-dos", "Pur&ge Completed"), this, SIGNAL(purgeCompletedSignal())); mPriorityPopupMenu = new QMenu(this); mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu unspecified priority", "unspecified")) ] = 0; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu highest priority", "1 (highest)")) ] = 1; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=2", "2")) ] = 2; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=3", "3")) ] = 3; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=4", "4")) ] = 4; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu medium priority", "5 (medium)")) ] = 5; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=6", "6")) ] = 6; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=7", "7")) ] = 7; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu priority value=8", "8")) ] = 8; mPriority[ mPriorityPopupMenu->addAction( i18nc("@action:inmenu lowest priority", "9 (lowest)")) ] = 9; connect(mPriorityPopupMenu, &QMenu::triggered, this, &TodoView::setNewPriority); mPercentageCompletedPopupMenu = new QMenu(this); for (int i = 0; i <= 100; i += 10) { const QString label = QStringLiteral("%1 %").arg(i); mPercentage[mPercentageCompletedPopupMenu->addAction(label)] = i; } connect(mPercentageCompletedPopupMenu, &QMenu::triggered, this, &TodoView::setNewPercentage); setMinimumHeight(50); // Initialize our proxy models setFlatView(preferences()->flatListTodo()); setFullView(preferences()->fullViewTodo()); updateConfig(); } TodoView::~TodoView() { saveViewState(); sModels->unregisterView(this); if (sModels->views.isEmpty()) { delete sModels; sModels = nullptr; } } void TodoView::expandIndex(const QModelIndex &index) { QModelIndex todoModelIndex = sModels->todoModel->mapFromSource(index); Q_ASSERT(todoModelIndex.isValid()); QModelIndex realIndex = mProxyModel->mapFromSource(todoModelIndex); Q_ASSERT(realIndex.isValid()); while (realIndex.isValid()) { mView->expand(realIndex); realIndex = mProxyModel->parent(realIndex); } } void TodoView::setCalendar(const Akonadi::ETMCalendar::Ptr &calendar) { EventView::setCalendar(calendar); if (!mSidebarView) { mQuickSearch->setCalendar(calendar); } mCategoriesDelegate->setCalendar(calendar); sModels->setCalendar(calendar); restoreViewState(); } Akonadi::Item::List TodoView::selectedIncidences() const { Akonadi::Item::List ret; const QModelIndexList selection = mView->selectionModel()->selectedRows(); ret.reserve(selection.count()); for (const QModelIndex &mi : selection) { ret << mi.data(TodoModel::TodoRole).value(); } return ret; } DateList TodoView::selectedIncidenceDates() const { // The todo view only lists todo's. It's probably not a good idea to // return something about the selected todo here, because it has got // a couple of dates (creation, due date, completion date), and the // caller could not figure out what he gets. So just return an empty list. return DateList(); } void TodoView::saveLayout(KConfig *config, const QString &group) const { KConfigGroup cfgGroup = config->group(group); QHeaderView *header = mView->header(); QVariantList columnVisibility; QVariantList columnOrder; QVariantList columnWidths; const int headerCount = header->count(); columnVisibility.reserve(headerCount); columnWidths.reserve(headerCount); columnOrder.reserve(headerCount); for (int i = 0; i < headerCount; ++i) { columnVisibility << QVariant(!mView->isColumnHidden(i)); columnWidths << QVariant(header->sectionSize(i)); columnOrder << QVariant(header->visualIndex(i)); } cfgGroup.writeEntry("ColumnVisibility", columnVisibility); cfgGroup.writeEntry("ColumnOrder", columnOrder); cfgGroup.writeEntry("ColumnWidths", columnWidths); cfgGroup.writeEntry("SortAscending", (int)header->sortIndicatorOrder()); if (header->isSortIndicatorShown()) { cfgGroup.writeEntry("SortColumn", header->sortIndicatorSection()); } else { cfgGroup.writeEntry("SortColumn", -1); } if (!mSidebarView) { preferences()->setFullViewTodo(mFullViewButton->isChecked()); } preferences()->setFlatListTodo(mFlatViewButton->isChecked()); } void TodoView::restoreLayout(KConfig *config, const QString &group, bool minimalDefaults) { KConfigGroup cfgGroup = config->group(group); QHeaderView *header = mView->header(); QVariantList columnVisibility = cfgGroup.readEntry("ColumnVisibility", QVariantList()); QVariantList columnOrder = cfgGroup.readEntry("ColumnOrder", QVariantList()); QVariantList columnWidths = cfgGroup.readEntry("ColumnWidths", QVariantList()); if (columnVisibility.isEmpty()) { // if config is empty then use default settings mView->hideColumn(TodoModel::RecurColumn); mView->hideColumn(TodoModel::DescriptionColumn); mView->hideColumn(TodoModel::CalendarColumn); if (minimalDefaults) { mView->hideColumn(TodoModel::PriorityColumn); mView->hideColumn(TodoModel::PercentColumn); mView->hideColumn(TodoModel::DescriptionColumn); mView->hideColumn(TodoModel::CategoriesColumn); } // We don't have any incidences (content) yet, so we delay resizing QTimer::singleShot(0, this, &TodoView::resizeColumns); } else { for (int i = 0; i < header->count() && i < columnOrder.size() && i < columnWidths.size() && i < columnVisibility.size(); i++) { bool visible = columnVisibility[i].toBool(); int width = columnWidths[i].toInt(); int order = columnOrder[i].toInt(); header->resizeSection(i, width); header->moveSection(header->visualIndex(i), order); if (i != 0 && !visible) { mView->hideColumn(i); } } } int sortOrder = cfgGroup.readEntry("SortAscending", (int)Qt::AscendingOrder); int sortColumn = cfgGroup.readEntry("SortColumn", -1); if (sortColumn >= 0) { mView->sortByColumn(sortColumn, (Qt::SortOrder)sortOrder); } mFlatViewButton->setChecked(cfgGroup.readEntry("FlatView", false)); } void TodoView::setIncidenceChanger(Akonadi::IncidenceChanger *changer) { EventView::setIncidenceChanger(changer); sModels->todoModel->setIncidenceChanger(changer); } void TodoView::showDates(const QDate &start, const QDate &end, const QDate &) { // There is nothing to do here for the Todo View Q_UNUSED(start); Q_UNUSED(end); } void TodoView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date) { Q_UNUSED(incidenceList); Q_UNUSED(date); } void TodoView::updateView() { // View is always updated, it's connected to ETM. } void TodoView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType) { // Don't do anything, model is connected to ETM, it's up to date } void TodoView::updateConfig() { Q_ASSERT(preferences()); if (!mSidebarView && mQuickSearch) { mQuickSearch->setVisible(preferences()->enableTodoQuickSearch()); } if (mQuickAdd) { mQuickAdd->setVisible(preferences()->enableQuickTodo()); } updateView(); } void TodoView::clearSelection() { mView->selectionModel()->clearSelection(); } void TodoView::addTodo(const QString &summary, const Akonadi::Item &parentItem, const QStringList &categories) { const QString summaryTrimmed = summary.trimmed(); if (!changer() || summaryTrimmed.isEmpty()) { return; } KCalCore::Todo::Ptr parent = CalendarSupport::todo(parentItem); KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(summaryTrimmed); todo->setOrganizer( Person::Ptr(new Person(CalendarSupport::KCalPrefs::instance()->fullName(), CalendarSupport::KCalPrefs::instance()->email()))); todo->setCategories(categories); if (parent && !parent->hasRecurrenceId()) { todo->setRelatedTo(parent->uid()); } Akonadi::Collection collection; // Use the same collection of the parent. if (parentItem.isValid()) { // Don't use parentColection() since it might be a virtual collection collection = calendar()->collection(parentItem.storageCollectionId()); } changer()->createIncidence(todo, Akonadi::Collection(), this); } void TodoView::addQuickTodo(Qt::KeyboardModifiers modifiers) { if (modifiers == Qt::NoModifier) { /*const QModelIndex index = */ addTodo(mQuickAdd->text(), Akonadi::Item(), mProxyModel->categories()); } else if (modifiers == Qt::ControlModifier) { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.count() != 1) { qCWarning(CALENDARVIEW_LOG) << "No to-do selected" << selection; return; } const QModelIndex idx = mProxyModel->mapToSource(selection[0]); mView->expand(selection[0]); const Akonadi::Item parent = sModels->todoModel->data(idx, Akonadi::EntityTreeModel::ItemRole). value(); addTodo(mQuickAdd->text(), parent, mProxyModel->categories()); } else { return; } mQuickAdd->setText(QString()); } void TodoView::contextMenu(const QPoint &pos) { const bool hasItem = mView->indexAt(pos).isValid(); Incidence::Ptr incidencePtr; for (QAction *entry : qAsConst(mItemPopupMenuItemOnlyEntries)) { bool enable; if (hasItem) { const Akonadi::Item::List incidences = selectedIncidences(); if (incidences.isEmpty()) { enable = false; } else { Akonadi::Item item = incidences.first(); incidencePtr = CalendarSupport::incidence(item); // Action isn't RO, it can change the incidence, "Edit" for example. const bool actionIsRw = mItemPopupMenuReadWriteEntries.contains(entry); const bool incidenceIsRO = !calendar()->hasRight(item, Akonadi::Collection::CanChangeItem); enable = hasItem && (!actionIsRw || !incidenceIsRO); } } else { enable = false; } entry->setEnabled(enable); } mCopyPopupMenu->setEnabled(hasItem); mMovePopupMenu->setEnabled(hasItem); if (hasItem) { if (incidencePtr) { const bool hasRecId = incidencePtr->hasRecurrenceId(); if (calendar()) { mMakeSubtodosIndependent->setEnabled( !hasRecId && !calendar()->childItems(incidencePtr->uid()).isEmpty()); } mMakeTodoIndependent->setEnabled(!hasRecId && !incidencePtr->relatedTo().isEmpty()); } switch (mView->indexAt(pos).column()) { case TodoModel::PriorityColumn: mPriorityPopupMenu->popup(mView->viewport()->mapToGlobal(pos)); break; case TodoModel::PercentColumn: mPercentageCompletedPopupMenu->popup(mView->viewport()->mapToGlobal(pos)); break; case TodoModel::StartDateColumn: case TodoModel::DueDateColumn: mMovePopupMenu->popup(mView->viewport()->mapToGlobal(pos)); break; case TodoModel::CategoriesColumn: createCategoryPopupMenu()->popup(mView->viewport()->mapToGlobal(pos)); break; default: mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos)); break; } } else { mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos)); } } void TodoView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); QModelIndexList selection = selected.indexes(); if (selection.isEmpty() || !selection[0].isValid()) { Q_EMIT incidenceSelected(Akonadi::Item(), QDate()); return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); if (selectedIncidenceDates().isEmpty()) { Q_EMIT incidenceSelected(todoItem, QDate()); } else { Q_EMIT incidenceSelected(todoItem, selectedIncidenceDates().at(0)); } } void TodoView::showTodo() { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); Q_EMIT showIncidenceSignal(todoItem); } void TodoView::editTodo() { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); Q_EMIT editIncidenceSignal(todoItem); } void TodoView::deleteTodo() { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() == 1) { const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); if (!changer()->deletedRecently(todoItem.id())) { Q_EMIT deleteIncidenceSignal(todoItem); } } } void TodoView::newTodo() { Q_EMIT newTodoSignal(QDate::currentDate().addDays(7)); } void TodoView::newSubTodo() { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() == 1) { const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); Q_EMIT newSubTodoSignal(todoItem); } else { // This never happens qCWarning(CALENDARVIEW_LOG) << "Selection size isn't 1"; } } void TodoView::copyTodoToDate(const QDate &date) { if (!changer()) { return; } QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const QModelIndex origIndex = mProxyModel->mapToSource(selection[0]); Q_ASSERT(origIndex.isValid()); const Akonadi::Item origItem = sModels->todoModel->data(origIndex, Akonadi::EntityTreeModel::ItemRole).value(); const KCalCore::Todo::Ptr orig = CalendarSupport::todo(origItem); if (!orig) { return; } KCalCore::Todo::Ptr todo(orig->clone()); todo->setUid(KCalCore::CalFormat::createUniqueId()); QDateTime due = todo->dtDue(); due.setDate(date); todo->setDtDue(due); changer()->createIncidence(todo, Akonadi::Collection(), this); } void TodoView::scheduleResizeColumns() { mResizeColumnsScheduled = true; mResizeColumnsTimer->start(); // restarts the timer if already active } void TodoView::itemDoubleClicked(const QModelIndex &index) { if (index.isValid()) { QModelIndex summary = index.sibling(index.row(), TodoModel::SummaryColumn); if (summary.flags() & Qt::ItemIsEditable) { editTodo(); } else { showTodo(); } } } QMenu *TodoView::createCategoryPopupMenu() { QMenu *tempMenu = new QMenu(this); QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return tempMenu; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); Q_ASSERT(todo); const QStringList checkedCategories = todo->categories(); Akonadi::TagFetchJob *tagFetchJob = new Akonadi::TagFetchJob(this); connect(tagFetchJob, &Akonadi::TagFetchJob::result, this, &TodoView::onTagsFetched); tagFetchJob->setProperty("menu", QVariant::fromValue(QPointer(tempMenu))); tagFetchJob->setProperty("checkedCategories", checkedCategories); connect(tempMenu, &QMenu::triggered, this, &TodoView::changedCategories); connect(tempMenu, &QMenu::aboutToHide, tempMenu, &QMenu::deleteLater); return tempMenu; } void TodoView::onTagsFetched(KJob *job) { if (job->error()) { qCWarning(CALENDARVIEW_LOG) << "Failed to fetch tags " << job->errorString(); return; } Akonadi::TagFetchJob *fetchJob = static_cast(job); const QStringList checkedCategories = job->property("checkedCategories").toStringList(); QPointer menu = job->property("menu").value >(); if (menu) { Q_FOREACH (const Akonadi::Tag &tag, fetchJob->tags()) { const QString name = tag.name(); QAction *action = menu->addAction(name); action->setCheckable(true); action->setData(name); if (checkedCategories.contains(name)) { action->setChecked(true); } } } } void TodoView::setNewDate(const QDate &date) { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); Q_ASSERT(todo); if (calendar()->hasRight(todoItem, Akonadi::Collection::CanChangeItem)) { KCalCore::Todo::Ptr oldTodo(todo->clone()); QDateTime dt(date); if (!todo->allDay()) { dt.setTime(todo->dtDue().time()); } todo->setDtDue(dt); changer()->modifyIncidence(todoItem, oldTodo, this); } else { qCDebug(CALENDARVIEW_LOG) << "Item is readOnly"; } } void TodoView::setNewPercentage(QAction *action) { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); Q_ASSERT(todo); if (calendar()->hasRight(todoItem, Akonadi::Collection::CanChangeItem)) { KCalCore::Todo::Ptr oldTodo(todo->clone()); int percentage = mPercentage.value(action); if (percentage == 100) { todo->setCompleted(QDateTime::currentDateTime()); todo->setPercentComplete(100); } else { todo->setPercentComplete(percentage); } if (todo->recurs() && percentage == 100) { changer()->modifyIncidence(todoItem, oldTodo, this); } else { changer()->modifyIncidence(todoItem, oldTodo, this); } } else { qCDebug(CALENDARVIEW_LOG) << "Item is read only"; } } void TodoView::setNewPriority(QAction *action) { const QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); if (calendar()->hasRight(todoItem, Akonadi::Collection::CanChangeItem)) { KCalCore::Todo::Ptr oldTodo(todo->clone()); todo->setPriority(mPriority[action]); changer()->modifyIncidence(todoItem, oldTodo, this); } } void TodoView::changedCategories(QAction *action) { const QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); Q_ASSERT(todo); if (calendar()->hasRight(todoItem, Akonadi::Collection::CanChangeItem)) { KCalCore::Todo::Ptr oldTodo(todo->clone()); const QString cat = action->data().toString(); QStringList categories = todo->categories(); if (categories.contains(cat)) { categories.removeAll(cat); } else { categories.append(cat); } categories.sort(); todo->setCategories(categories); changer()->modifyIncidence(todoItem, oldTodo, this); } else { qCDebug(CALENDARVIEW_LOG) << "No active item, active item is read-only, or locking failed"; } } void TodoView::setFullView(bool fullView) { if (!mFullViewButton) { return; } mFullViewButton->setChecked(fullView); if (fullView) { mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-restore"))); } else { mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); } mFullViewButton->blockSignals(true); // We block signals to avoid recursion; there are two TodoViews and // also mFullViewButton is synchronized. mFullViewButton->setChecked(fullView); mFullViewButton->blockSignals(false); preferences()->setFullViewTodo(fullView); preferences()->writeConfig(); Q_EMIT fullViewChanged(fullView); } void TodoView::setFlatView(bool flatView, bool notifyOtherViews) { if (flatView) { mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); } else { mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details"))); } if (notifyOtherViews) { sModels->setFlatView(flatView); } } void TodoView::onRowsInserted(const QModelIndex &parent, int start, int end) { if (start != end || !calendar() || !calendar()->entityTreeModel()) { return; } QModelIndex idx = mView->model()->index(start, 0); // If the collection is currently being populated, we don't do anything QVariant v = idx.data(Akonadi::EntityTreeModel::ItemRole); if (!v.isValid()) { return; } Akonadi::Item item = v.value(); if (!item.isValid()) { return; } const bool isPopulated = calendar()->entityTreeModel()->isCollectionPopulated( item.storageCollectionId()); if (!isPopulated) { return; } // Case #1, adding an item that doesn't have parent: We select it if (!parent.isValid()) { QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() <= 1) { // don't destroy complex selections, not applicable now (only single // selection allowed), but for the future... int colCount = static_cast(TodoModel::ColumnCount); mView->selectionModel()->select( QItemSelection(idx, mView->model()->index(start, colCount - 1)), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } return; } // Case 2: Adding an item that has a parent: we expand the parent if (sModels->isFlatView()) { return; } QModelIndex index = parent; mView->expand(index); while (index.parent().isValid()) { mView->expand(index.parent()); index = index.parent(); } } void TodoView::getHighlightMode(bool &highlightEvents, bool &highlightTodos, bool &highlightJournals) { highlightTodos = preferences()->highlightTodos(); highlightEvents = !highlightTodos; highlightJournals = false; } bool TodoView::usesFullWindow() { return preferences()->fullViewTodo(); } void TodoView::resizeColumns() { mResizeColumnsScheduled = false; mView->resizeColumnToContents(TodoModel::StartDateColumn); mView->resizeColumnToContents(TodoModel::DueDateColumn); mView->resizeColumnToContents(TodoModel::PriorityColumn); mView->resizeColumnToContents(TodoModel::CalendarColumn); mView->resizeColumnToContents(TodoModel::RecurColumn); mView->resizeColumnToContents(TodoModel::PercentColumn); // We have 3 columns that should stretch: summary, description and categories. // Summary is always visible. const bool descriptionVisible = !mView->isColumnHidden(TodoModel::DescriptionColumn); const bool categoriesVisible = !mView->isColumnHidden(TodoModel::CategoriesColumn); // Calculate size of non-stretchable columns: int size = 0; for (int i = 0; i < TodoModel::ColumnCount; ++i) { if (!mView->isColumnHidden(i) && i != TodoModel::SummaryColumn && i != TodoModel::DescriptionColumn && i != TodoModel::CategoriesColumn) { size += mView->columnWidth(i); } } // Calculate the remaining space that we have for the stretchable columns int remainingSize = mView->header()->width() - size; // 100 for summary, 100 for description const int requiredSize = descriptionVisible ? 200 : 100; if (categoriesVisible) { const int categorySize = 100; mView->setColumnWidth(TodoModel::CategoriesColumn, categorySize); remainingSize -= categorySize; } if (remainingSize < requiredSize) { // Too little size, so let's use a horizontal scrollbar mView->resizeColumnToContents(TodoModel::SummaryColumn); mView->resizeColumnToContents(TodoModel::DescriptionColumn); } else if (descriptionVisible) { mView->setColumnWidth(TodoModel::SummaryColumn, remainingSize / 2); mView->setColumnWidth(TodoModel::DescriptionColumn, remainingSize / 2); } else { mView->setColumnWidth(TodoModel::SummaryColumn, remainingSize); } } void TodoView::restoreViewState() { if (sModels->isFlatView()) { return; } if (sModels->todoTreeModel && !sModels->todoTreeModel->sourceModel()) { return; } //QElapsedTimer timer; //timer.start(); delete mTreeStateRestorer; mTreeStateRestorer = new Akonadi::ETMViewStateSaver(); KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group(config, stateSaverGroup()); mTreeStateRestorer->setView(mView); mTreeStateRestorer->restoreState(group); //qCDebug(CALENDARVIEW_LOG) << "Took " << timer.elapsed(); } QString TodoView::stateSaverGroup() const { QString str = QStringLiteral("TodoTreeViewState"); if (mSidebarView) { str += QLatin1Char('S'); } return str; } void TodoView::saveViewState() { Akonadi::ETMViewStateSaver treeStateSaver; KConfigGroup group(preferences()->config(), stateSaverGroup()); treeStateSaver.setView(mView); treeStateSaver.saveState(group); } void TodoView::resizeEvent(QResizeEvent *event) { EventViews::EventView::resizeEvent(event); scheduleResizeColumns(); } void TodoView::createEvent() { const QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); Q_EMIT createEvent(todoItem); } void TodoView::createNote() { const QModelIndexList selection = mView->selectionModel()->selectedRows(); if (selection.size() != 1) { return; } const Akonadi::Item todoItem = selection[0].data(TodoModel::TodoRole).value(); Q_EMIT createNote(todoItem); }