diff --git a/kdeui/itemviews/kcategorizedview.cpp b/kdeui/itemviews/kcategorizedview.cpp index 4b30859..005102a 100644 --- a/kdeui/itemviews/kcategorizedview.cpp +++ b/kdeui/itemviews/kcategorizedview.cpp @@ -1,6 +1,6 @@ /** * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007, 2008 Rafael Fernández López * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -33,21 +33,19 @@ #include "kcategorydrawer.h" #include "kcategorizedsortfilterproxymodel.h" -// By defining DOLPHIN_DRAGANDDROP the custom drag and drop implementation of -// KCategorizedView is bypassed to have a consistent drag and drop look for all -// views. Hopefully transparent pixmaps for drag objects will be supported in -// Qt 4.4, so that this workaround can be skipped. -#define DOLPHIN_DRAGANDDROP +//BEGIN: Private part -KCategorizedView::Private::Private(KCategorizedView *listView) - : listView(listView) - , categoryDrawer(0) - , biggestItemSize(QSize(0, 0)) - , mouseButtonPressed(false) - , rightMouseButtonPressed(false) - , isDragging(false) - , dragLeftViewport(false) +KCategorizedView::Private::Private(KCategorizedView *q) + : q(q) , proxyModel(0) + , categoryDrawer(0) + , categoryDrawerV2(0) + , categorySpacing(5) + , alternatingBlockColors(false) + , collapsibleBlocks(false) + , hoveredIndex(QModelIndex()) + , pressedPosition(QPoint()) + , rubberBandRect(QRect()) { } @@ -55,483 +53,325 @@ KCategorizedView::Private::~Private() { } -const QModelIndexList &KCategorizedView::Private::intersectionSet(const QRect &rect) +bool KCategorizedView::Private::isCategorized() const { - QModelIndex index; - QRect indexVisualRect; - - intersectedIndexes.clear(); + return proxyModel && categoryDrawer && proxyModel->isCategorizedModel() && + q->flow() == QListView::LeftToRight; +} - int itemHeight; +QPair KCategorizedView::Private::intersectingIndexesWithRect(const QRect &_rect) const +{ + const int rowCount = proxyModel->rowCount(); - if (listView->gridSize().isEmpty()) - { - itemHeight = biggestItemSize.height(); - } - else - { - itemHeight = listView->gridSize().height(); - } + const QRect rect = _rect.normalized(); - // Lets find out where we should start - int top = proxyModel->rowCount() - 1; + // binary search to find out the top border int bottom = 0; - int middle = (top + bottom) / 2; - while (bottom <= top) - { - middle = (top + bottom) / 2; - - index = proxyModel->index(middle, 0); - indexVisualRect = visualRect(index); - // We need the whole height (not only the visualRect). This will help us to update - // all needed indexes correctly (ereslibre) - indexVisualRect.setHeight(indexVisualRect.height() + (itemHeight - indexVisualRect.height())); - - if (qMax(indexVisualRect.topLeft().y(), - indexVisualRect.bottomRight().y()) < qMin(rect.topLeft().y(), - rect.bottomRight().y())) - { + int top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex()); + QRect itemRect = q->visualRect(index); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if (itemRect.bottomRight().y() <= rect.topLeft().y()) { bottom = middle + 1; - } - else - { + } else { top = middle - 1; } } - for (int i = middle; i < proxyModel->rowCount(); i++) - { - index = proxyModel->index(i, 0); - indexVisualRect = visualRect(index); - - if (rect.intersects(indexVisualRect)) - intersectedIndexes.append(index); - - // If we passed next item, stop searching for hits - if (qMax(rect.bottomRight().y(), rect.topLeft().y()) < - qMin(indexVisualRect.topLeft().y(), - indexVisualRect.bottomRight().y())) - break; + const QModelIndex bottomIndex = proxyModel->index(bottom, q->modelColumn(), q->rootIndex()); + + // binary search to find out the bottom border + bottom = 0; + top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex()); + QRect itemRect = q->visualRect(index); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if (itemRect.topLeft().y() <= rect.bottomRight().y()) { + bottom = middle + 1; + } else { + top = middle - 1; + } } - return intersectedIndexes; + const QModelIndex topIndex = proxyModel->index(top, q->modelColumn(), q->rootIndex()); + + return qMakePair(bottomIndex, topIndex); } -QRect KCategorizedView::Private::visualRectInViewport(const QModelIndex &index) const +struct KCategorizedView::Private::Item { - if (!index.isValid()) - return QRect(); - - QString curCategory = elementsInfo[index.row()].category; - - QRect retRect; - const bool leftToRightFlow = (listView->flow() == QListView::LeftToRight); - - if (leftToRightFlow) - { - if (listView->layoutDirection() == Qt::LeftToRight) - { - retRect = QRect(listView->spacing(), listView->spacing() * 2 + - categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); - } - else - { - retRect = QRect(listView->viewport()->width() - listView->spacing(), listView->spacing() * 2 + - categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); - } - } - else + Item() + : topLeft(QPoint()) + , size(QSize()) { - retRect = QRect(listView->spacing(), listView->spacing() * 2 + - categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); } - int viewportWidth = listView->viewport()->width() - listView->spacing(); + QPoint topLeft; + QSize size; +}; - int itemHeight; - int itemWidth; +struct KCategorizedView::Private::Block +{ + Block() + : topLeft(QPoint()) + , height(-1) + , firstIndex(QModelIndex()) + , quarantineStart(QModelIndex()) + , items(QList()) + , outOfQuarantine(false) + , alternate(false) + , collapsed(false) + { + } + + static bool lessThan(const Block &left, const Block &right) + { + Q_ASSERT(left.firstIndex.isValid()); + Q_ASSERT(right.firstIndex.isValid()); + return left.firstIndex.row() < right.firstIndex.row(); + } + + QPoint topLeft; + int height; + QPersistentModelIndex firstIndex; + // if we have n elements on this block, and we inserted an element at position i. The quarantine + // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the + // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine + // will only affect the current block, since the rest of blocks can be affected only in the way + // that the whole block will have different offset, but items will keep the same relative position + // in terms of their parent blocks. + QPersistentModelIndex quarantineStart; + QList items; + + // this affects the whole block, not items separately. items contain the topLeft point relative + // to the block. Because of insertions or removals a whole block can be moved, so the whole block + // will enter in quarantine, what is faster than moving all items in absolute terms. + bool outOfQuarantine; + + // should we alternate its color ? is just a hint, could not be used + bool alternate; + bool collapsed; +}; + +QPoint KCategorizedView::Private::blockPosition(const QString &category) +{ + Block &block = blocks[category]; - if (listView->gridSize().isEmpty() && leftToRightFlow) - { - itemHeight = biggestItemSize.height(); - itemWidth = biggestItemSize.width(); - } - else if (leftToRightFlow) - { - itemHeight = listView->gridSize().height(); - itemWidth = listView->gridSize().width(); - } - else if (listView->gridSize().isEmpty() && !leftToRightFlow) - { - itemHeight = biggestItemSize.height(); - itemWidth = listView->viewport()->width() - listView->spacing() * 2; - } - else - { - itemHeight = listView->gridSize().height(); - itemWidth = listView->gridSize().width() - listView->spacing() * 2; + if (block.outOfQuarantine && !block.topLeft.isNull()) { + return block.topLeft; } - int itemWidthPlusSeparation = listView->spacing() + itemWidth; - if (!itemWidthPlusSeparation) - itemWidthPlusSeparation++; - int elementsPerRow = viewportWidth / itemWidthPlusSeparation; - if (!elementsPerRow) - elementsPerRow++; - - int column; - int row; + QPoint res(categorySpacing, 0); - if (leftToRightFlow) - { - column = elementsInfo[index.row()].relativeOffsetToCategory % elementsPerRow; - row = elementsInfo[index.row()].relativeOffsetToCategory / elementsPerRow; - - if (listView->layoutDirection() == Qt::LeftToRight) - { - retRect.setLeft(retRect.left() + column * listView->spacing() + - column * itemWidth); - } - else - { - retRect.setLeft(retRect.right() - column * listView->spacing() - - column * itemWidth - itemWidth); + const QModelIndex index = block.firstIndex; - retRect.setRight(retRect.right() - column * listView->spacing() - - column * itemWidth); + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Block &block = *it; + const QModelIndex categoryIndex = block.firstIndex; + if (index.row() < categoryIndex.row()) { + continue; } - } - else - { - elementsPerRow = 1; - column = elementsInfo[index.row()].relativeOffsetToCategory % elementsPerRow; - row = elementsInfo[index.row()].relativeOffsetToCategory / elementsPerRow; - } - - foreach (const QString &category, categories) - { - if (category == curCategory) - break; - - float rows = (float) ((float) categoriesIndexes[category].count() / - (float) elementsPerRow); - - int rowsInt = categoriesIndexes[category].count() / elementsPerRow; - - if (rows - trunc(rows)) rowsInt++; - - retRect.setTop(retRect.top() + - (rowsInt * itemHeight) + - categoryDrawer->categoryHeight(index, listView->viewOptions()) + - listView->spacing() * 2); - - if (listView->gridSize().isEmpty()) - { - retRect.setTop(retRect.top() + - (rowsInt * listView->spacing())); + res.ry() += categoryDrawer->categoryHeight(categoryIndex, q->viewOptions()) + categorySpacing; + if (index.row() == categoryIndex.row()) { + continue; } + res.ry() += blockHeight(it.key()); } - if (listView->gridSize().isEmpty()) - { - retRect.setTop(retRect.top() + row * listView->spacing() + - (row * itemHeight)); - } - else - { - retRect.setTop(retRect.top() + (row * itemHeight)); - } - - retRect.setWidth(itemWidth); - - QModelIndex heightIndex = proxyModel->index(index.row(), 0); - if (listView->gridSize().isEmpty()) - { - retRect.setHeight(listView->sizeHintForIndex(heightIndex).height()); - } - else - { - const QSize sizeHint = listView->sizeHintForIndex(heightIndex); - if (sizeHint.width() < itemWidth && leftToRightFlow) { - retRect.setWidth(sizeHint.width()); - retRect.moveLeft(retRect.left() + (itemWidth - sizeHint.width()) / 2); - } - retRect.setHeight(qMin(sizeHint.height(), listView->gridSize().height())); - } + block.outOfQuarantine = true; + block.topLeft = res; - return retRect; + return res; } -QRect KCategorizedView::Private::visualCategoryRectInViewport(const QString &category) const +int KCategorizedView::Private::blockHeight(const QString &category) { - QRect retRect(listView->spacing(), - listView->spacing(), - listView->viewport()->width() - listView->spacing() * 2, - 0); - - if (!proxyModel || !categoryDrawer || !proxyModel->isCategorizedModel() || !proxyModel->rowCount() || !categories.contains(category)) - return QRect(); - - QModelIndex index = proxyModel->index(0, 0, QModelIndex()); + Block &block = blocks[category]; - int viewportWidth = listView->viewport()->width() - listView->spacing(); - - int itemHeight; - int itemWidth; - - if (listView->gridSize().isEmpty()) - { - itemHeight = biggestItemSize.height(); - itemWidth = biggestItemSize.width(); - } - else - { - itemHeight = listView->gridSize().height(); - itemWidth = listView->gridSize().width(); + if (block.collapsed) { + return 0; } - int itemWidthPlusSeparation = listView->spacing() + itemWidth; - int elementsPerRow = viewportWidth / itemWidthPlusSeparation; - - if (!elementsPerRow) - elementsPerRow++; - - if (listView->flow() == QListView::TopToBottom) - { - elementsPerRow = 1; + if (block.height > -1) { + return block.height; } - foreach (const QString &itCategory, categories) - { - if (itCategory == category) - break; - - float rows = (float) ((float) categoriesIndexes[itCategory].count() / - (float) elementsPerRow); - int rowsInt = categoriesIndexes[itCategory].count() / elementsPerRow; - - if (rows - trunc(rows)) rowsInt++; + const QModelIndex firstIndex = block.firstIndex; + const QModelIndex lastIndex = proxyModel->index(firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex()); + const QRect topLeft = q->visualRect(firstIndex); + QRect bottomRight = q->visualRect(lastIndex); - retRect.setTop(retRect.top() + - (rowsInt * itemHeight) + - categoryDrawer->categoryHeight(index, listView->viewOptions()) + - listView->spacing() * 2); - - if (listView->gridSize().isEmpty()) - { - retRect.setTop(retRect.top() + - (rowsInt * listView->spacing())); + if (hasGrid()) { + bottomRight.setHeight(qMax(bottomRight.height(), q->gridSize().height())); + } else { + if (!q->uniformItemSizes()) { + bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2); } } - retRect.setHeight(categoryDrawer->categoryHeight(index, listView->viewOptions())); - - return retRect; -} - -// We're sure elementsPosition doesn't contain index -const QRect &KCategorizedView::Private::cacheIndex(const QModelIndex &index) -{ - QRect rect = visualRectInViewport(index); - QHash::iterator it = elementsPosition.insert(index.row(), rect); + const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1; + block.height = height; - return *it; + return height; } -// We're sure categoriesPosition doesn't contain category -const QRect &KCategorizedView::Private::cacheCategory(const QString &category) +int KCategorizedView::Private::viewportWidth() const { - QRect rect = visualCategoryRectInViewport(category); - QHash::iterator it = categoriesPosition.insert(category, rect); - - return *it; + return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin(); } -const QRect &KCategorizedView::Private::cachedRectIndex(const QModelIndex &index) +void KCategorizedView::Private::regenerateAllElements() { - QHash::const_iterator it = elementsPosition.constFind(index.row()); - if (it != elementsPosition.constEnd()) // If we have it cached - { // return it - return *it; - } - else // Otherwise, cache it - { // and return it - return cacheIndex(index); + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Block &block = *it; + block.outOfQuarantine = false; + block.quarantineStart = block.firstIndex; + block.height = -1; } } -const QRect &KCategorizedView::Private::cachedRectCategory(const QString &category) +void KCategorizedView::Private::rowsInserted(const QModelIndex &parent, int start, int end) { - QHash::const_iterator it = categoriesPosition.constFind(category); - if (it != categoriesPosition.constEnd()) // If we have it cached - { // return it - return *it; - } - else // Otherwise, cache it and - { // return it - return cacheCategory(category); + if (!isCategorized()) { + return; } -} - -QRect KCategorizedView::Private::visualRect(const QModelIndex &index) -{ - QRect retRect = cachedRectIndex(index); - int dx = -listView->horizontalOffset(); - int dy = -listView->verticalOffset(); - retRect.adjust(dx, dy, dx, dy); - return retRect; -} - -QRect KCategorizedView::Private::categoryVisualRect(const QString &category) -{ - QRect retRect = cachedRectCategory(category); - int dx = -listView->horizontalOffset(); - int dy = -listView->verticalOffset(); - retRect.adjust(dx, dy, dx, dy); + for (int i = start; i <= end; ++i) { + const QModelIndex index = proxyModel->index(i, q->modelColumn(), parent); - return retRect; -} + Q_ASSERT(index.isValid()); -void KCategorizedView::Private::drawNewCategory(const QModelIndex &index, - int sortRole, - const QStyleOption &option, - QPainter *painter) -{ - if (!index.isValid()) - { - return; - } + const QString category = categoryForIndex(index); - QStyleOption optionCopy = option; - const QString category = proxyModel->data(index, KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + Block &block = blocks[category]; - optionCopy.state &= ~QStyle::State_Selected; - - if ((listView->selectionMode() != SingleSelection) && (listView->selectionMode() != NoSelection)) { - if ((category == hoveredCategory) && !mouseButtonPressed) - { - optionCopy.state |= QStyle::State_MouseOver; - } - else if ((category == hoveredCategory) && mouseButtonPressed) - { - QPoint initialPressPosition = listView->viewport()->mapFromGlobal(QCursor::pos()); - initialPressPosition.setY(initialPressPosition.y() + listView->verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() + listView->horizontalOffset()); - - if (initialPressPosition == this->initialPressPosition) - { - optionCopy.state |= QStyle::State_Selected; - } + //BEGIN: update firstIndex + // save as firstIndex in block if + // - it forced the category creation (first element on this category) + // - it is before the first row on that category + const QModelIndex firstIndex = block.firstIndex; + if (!firstIndex.isValid() || index.row() < firstIndex.row()) { + block.firstIndex = index; } - } + //END: update firstIndex - categoryDrawer->drawCategory(index, - sortRole, - optionCopy, - painter); -} + Q_ASSERT(block.firstIndex.isValid()); + const int firstIndexRow = block.firstIndex.row(); -void KCategorizedView::Private::updateScrollbars() -{ - // find the last index in the last category - QModelIndex lastIndex = categoriesIndexes.isEmpty() ? QModelIndex() : categoriesIndexes[categories.last()].last(); + block.items.insert(index.row() - firstIndexRow, Private::Item()); - int lastItemBottom = cachedRectIndex(lastIndex).top() + - listView->spacing() + (listView->gridSize().isEmpty() ? biggestItemSize.height() : listView->gridSize().height()) - listView->viewport()->height(); + //TODO: assign less times + block.height = -1; - listView->horizontalScrollBar()->setRange(0, 0); + q->visualRect(index); + q->viewport()->update(); + } - if (listView->verticalScrollMode() == QAbstractItemView::ScrollPerItem) + //BEGIN: update the items that are in quarantine in affected categories { - listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + const QModelIndex lastIndex = proxyModel->index(end, q->modelColumn(), parent); + const QString category = categoryForIndex(lastIndex); + Private::Block &block = blocks[category]; + block.quarantineStart = block.firstIndex; } + //END: update the items that are in quarantine in affected categories - if (listView->horizontalScrollMode() == QAbstractItemView::ScrollPerItem) + //BEGIN: mark as in quarantine those categories that are under the affected ones { - listView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + const QModelIndex firstIndex = proxyModel->index(start, q->modelColumn(), parent); + const QString category = categoryForIndex(firstIndex); + const QModelIndex firstAffectedCategory = blocks[category].firstIndex; + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = blocks.values(); + qSort(blockList.begin(), blockList.end(), Block::lessThan); + QList firstIndexesRows; + foreach (const Block &block, blockList) { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Private::Block &block = *it; + if (block.firstIndex.row() > firstAffectedCategory.row()) { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } else if (block.firstIndex.row() == firstAffectedCategory.row()) { + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } + } } - - listView->verticalScrollBar()->setSingleStep(listView->viewport()->height() / 10); - listView->verticalScrollBar()->setPageStep(listView->viewport()->height()); - listView->verticalScrollBar()->setRange(0, lastItemBottom); + //END: mark as in quarantine those categories that are under the affected ones } -void KCategorizedView::Private::drawDraggedItems(QPainter *painter) +QRect KCategorizedView::Private::mapToViewport(const QRect &rect) const { - QStyleOptionViewItemV4 option = listView->viewOptions(); - option.state &= ~QStyle::State_MouseOver; - foreach (const QModelIndex &index, listView->selectionModel()->selectedIndexes()) - { - const int dx = mousePosition.x() - initialPressPosition.x() + listView->horizontalOffset(); - const int dy = mousePosition.y() - initialPressPosition.y() + listView->verticalOffset(); - - option.rect = visualRect(index); - option.rect.adjust(dx, dy, dx, dy); + const int dx = -q->horizontalOffset(); + const int dy = -q->verticalOffset(); + return rect.adjusted(dx, dy, dx, dy); +} - if (option.rect.intersects(listView->viewport()->rect())) - { - listView->itemDelegate(index)->paint(painter, option, index); - } - } +QRect KCategorizedView::Private::mapFromViewport(const QRect &rect) const +{ + const int dx = q->horizontalOffset(); + const int dy = q->verticalOffset(); + return rect.adjusted(dx, dy, dx, dy); } -void KCategorizedView::Private::layoutChanged(bool forceItemReload) +int KCategorizedView::Private::highestElementInLastRow(const Block &block) const { - if (proxyModel && categoryDrawer && proxyModel->isCategorizedModel() && - ((forceItemReload || - (modelSortRole != proxyModel->sortRole()) || - (modelSortColumn != proxyModel->sortColumn()) || - (modelSortOrder != proxyModel->sortOrder()) || - (modelLastRowCount != proxyModel->rowCount()) || - (modelCategorized != proxyModel->isCategorizedModel())))) - { - // Force the view to update all elements - listView->rowsInsertedArtifficial(QModelIndex(), 0, proxyModel->rowCount() - 1); - - if (!forceItemReload) - { - modelSortRole = proxyModel->sortRole(); - modelSortColumn = proxyModel->sortColumn(); - modelSortOrder = proxyModel->sortOrder(); - modelLastRowCount = proxyModel->rowCount(); - modelCategorized = proxyModel->isCategorizedModel(); + //Find the highest element in the last row + const QModelIndex lastIndex = proxyModel->index(block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex()); + QModelIndex prevIndex = proxyModel->index(lastIndex.row(), q->modelColumn(), q->rootIndex()); + QRect prevRect = q->visualRect(prevIndex); + int res = prevRect.height(); + Q_FOREVER { + prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex()); + const QRect tempRect = q->visualRect(prevIndex); + if (tempRect.topLeft().y() < prevRect.topLeft().y()) { + break; + } + res = qMax(res, tempRect.height()); + if (prevIndex == block.firstIndex) { + break; } } - if (proxyModel && categoryDrawer && proxyModel->isCategorizedModel()) - { - updateScrollbars(); - } + return res; } -void KCategorizedView::Private::drawDraggedItems() +bool KCategorizedView::Private::hasGrid() const { - QRect rectToUpdate; - QRect currentRect; - foreach (const QModelIndex &index, listView->selectionModel()->selectedIndexes()) - { - int dx = mousePosition.x() - initialPressPosition.x() + listView->horizontalOffset(); - int dy = mousePosition.y() - initialPressPosition.y() + listView->verticalOffset(); - - currentRect = visualRect(index); - currentRect.adjust(dx, dy, dx, dy); - - if (currentRect.intersects(listView->viewport()->rect())) - { - rectToUpdate = rectToUpdate.united(currentRect); - } - } - - listView->viewport()->update(lastDraggedItemsRect.united(rectToUpdate)); + const QSize gridSize = q->gridSize(); + return gridSize.isValid() && !gridSize.isNull(); +} - lastDraggedItemsRect = rectToUpdate; +QString KCategorizedView::Private::categoryForIndex(const QModelIndex &index) const +{ + const QModelIndex categoryIndex = index.model()->index(index.row(), proxyModel->sortColumn(), index.parent()); + return categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); } +void KCategorizedView::Private::_k_slotCollapseOrExpandClicked() +{ +} -//============================================================================== +//END: Private part +//BEGIN: Public part KCategorizedView::KCategorizedView(QWidget *parent) : QListView(parent) @@ -544,937 +384,722 @@ KCategorizedView::~KCategorizedView() delete d; } -void KCategorizedView::setGridSize(const QSize &size) -{ - QListView::setGridSize(size); - - d->layoutChanged(true); -} - void KCategorizedView::setModel(QAbstractItemModel *model) { - d->lastSelection = QItemSelection(); - d->forcedSelectionPosition = 0; - d->elementsInfo.clear(); - d->elementsPosition.clear(); - d->categoriesIndexes.clear(); - d->categoriesPosition.clear(); - d->categories.clear(); - d->intersectedIndexes.clear(); - d->modelIndexList.clear(); - d->hovered = QModelIndex(); - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - - if (d->proxyModel) - { - QObject::disconnect(d->proxyModel, - SIGNAL(layoutChanged()), - this, SLOT(slotLayoutChanged())); - - QObject::disconnect(d->proxyModel, - SIGNAL(rowsRemoved(QModelIndex,int,int)), - this, SLOT(rowsRemoved(QModelIndex,int,int))); + if (d->proxyModel == model) { + return; } - QListView::setModel(model); + d->blocks.clear(); + + if (d->proxyModel) { + disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); + } d->proxyModel = dynamic_cast(model); - if (d->proxyModel) - { - d->modelSortRole = d->proxyModel->sortRole(); - d->modelSortColumn = d->proxyModel->sortColumn(); - d->modelSortOrder = d->proxyModel->sortOrder(); - d->modelLastRowCount = d->proxyModel->rowCount(); - d->modelCategorized = d->proxyModel->isCategorizedModel(); - - QObject::connect(d->proxyModel, - SIGNAL(layoutChanged()), - this, SLOT(slotLayoutChanged())); - - QObject::connect(d->proxyModel, - SIGNAL(rowsRemoved(QModelIndex,int,int)), - this, SLOT(rowsRemoved(QModelIndex,int,int))); - - if (d->proxyModel->rowCount()) - { - d->layoutChanged(true); - } + if (d->proxyModel) { + connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); } - else - { - d->modelCategorized = false; + + QListView::setModel(model); + + // if the model already had information inserted, update our data structures to it + if (model->rowCount()) { + slotLayoutChanged(); } } +void KCategorizedView::setGridSize(const QSize &size) +{ + d->regenerateAllElements(); + QListView::setGridSize(size); +} + +// TODO: make difference depending on viewMode: ListMode or IconMode. QRect KCategorizedView::visualRect(const QModelIndex &index) const { - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { + if (!d->isCategorized()) { return QListView::visualRect(index); } - if (!qobject_cast(index.model())) - { - return d->visualRect(d->proxyModel->mapFromSource(index)); + if (!index.isValid()) { + return QRect(); } - return d->visualRect(index); -} + const QString category = d->categoryForIndex(index); -KCategoryDrawer *KCategorizedView::categoryDrawer() const -{ - return d->categoryDrawer; -} + if (!d->blocks.contains(category)) { + return QRect(); + } -void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer) -{ - d->lastSelection = QItemSelection(); - d->forcedSelectionPosition = 0; - d->elementsInfo.clear(); - d->elementsPosition.clear(); - d->categoriesIndexes.clear(); - d->categoriesPosition.clear(); - d->categories.clear(); - d->intersectedIndexes.clear(); - d->modelIndexList.clear(); - d->hovered = QModelIndex(); - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - - if (!categoryDrawer && d->proxyModel) - { - QObject::disconnect(d->proxyModel, - SIGNAL(layoutChanged()), - this, SLOT(slotLayoutChanged())); + Private::Block &block = d->blocks[category]; + int firstIndexRow = block.firstIndex.row(); - QObject::disconnect(d->proxyModel, - SIGNAL(rowsRemoved(QModelIndex,int,int)), - this, SLOT(rowsRemoved(QModelIndex,int,int))); - } - else if (categoryDrawer && d->proxyModel) - { - QObject::connect(d->proxyModel, - SIGNAL(layoutChanged()), - this, SLOT(slotLayoutChanged())); + Q_ASSERT(block.firstIndex.isValid()); - QObject::connect(d->proxyModel, - SIGNAL(rowsRemoved(QModelIndex,int,int)), - this, SLOT(rowsRemoved(QModelIndex,int,int))); + if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) { + return QRect(); } - d->categoryDrawer = categoryDrawer; + const QPoint blockPos = d->blockPosition(category); + + Private::Item &ritem = block.items[index.row() - firstIndexRow]; + + if (ritem.topLeft.isNull() || (block.quarantineStart.isValid() && + index.row() >= block.quarantineStart.row())) { + if (d->hasGrid()) { + const int relativeRow = index.row() - firstIndexRow; + const int maxItemsPerRow = qMax(d->viewportWidth() / gridSize().width(), 1); + ritem.topLeft.rx() = (relativeRow % maxItemsPerRow) * gridSize().width() + blockPos.x() + d->categoryDrawer->leftMargin(); + ritem.topLeft.ry() = (relativeRow / maxItemsPerRow) * gridSize().height(); + } else { + if (uniformItemSizes()) { + const int relativeRow = index.row() - firstIndexRow; + const QSize itemSize = sizeHintForIndex(index); + const int maxItemsPerRow = qMax((d->viewportWidth() - spacing()) / (itemSize.width() + spacing()), 1); + ritem.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + d->categoryDrawer->leftMargin(); + ritem.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height(); + } else { + if (index != block.firstIndex) { + const int viewportWidth = d->viewportWidth() - spacing(); + QModelIndex prevIndex = d->proxyModel->index(index.row() - 1, modelColumn(), rootIndex()); + QRect prevRect = visualRect(prevIndex); + prevRect = d->mapFromViewport(prevRect); + const QSize currSize = sizeHintForIndex(index); + if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + spacing() > viewportWidth) { + // we have to check the whole previous row, and see which one was the + // highest. + Q_FOREVER { + prevIndex = d->proxyModel->index(prevIndex.row() - 1, modelColumn(), rootIndex()); + const QRect tempRect = visualRect(prevIndex); + if (tempRect.topLeft().y() < prevRect.topLeft().y()) { + break; + } + if (tempRect.bottomRight().y() > prevRect.bottomRight().y()) { + prevRect = tempRect; + } + if (prevIndex == block.firstIndex) { + break; + } + } + ritem.topLeft.rx() = d->categoryDrawer->leftMargin() + blockPos.x() + spacing(); + ritem.topLeft.ry() = (prevRect.bottomRight().y() + 1) + spacing() - blockPos.y(); + } else { + ritem.topLeft.rx() = (prevRect.bottomRight().x() + 1) + spacing(); + ritem.topLeft.ry() = prevRect.topLeft().y() - blockPos.y(); + } + } else { + ritem.topLeft.rx() = blockPos.x() + d->categoryDrawer->leftMargin() + spacing(); + ritem.topLeft.ry() = spacing(); + } + } + } - if (categoryDrawer) - { - if (d->proxyModel) - { - if (d->proxyModel->rowCount()) - { - d->layoutChanged(true); + //TODO: update this cached size if the delegate emits sizeHintChanged + ritem.size = sizeHintForIndex(index); + + //BEGIN: update the quarantine start + const bool wasLastIndex = (index.row() == (block.firstIndex.row() + block.items.count() - 1)); + if (index.row() == block.quarantineStart.row()) { + if (wasLastIndex) { + block.quarantineStart = QModelIndex(); + } else { + const QModelIndex nextIndex = d->proxyModel->index(index.row() + 1, modelColumn(), rootIndex()); + block.quarantineStart = nextIndex; } } + //END: update the quarantine start + } + + // we get now the absolute position through the relative position of the parent block. do not + // save this on ritem, since this would override the item relative position in block terms. + Private::Item item(ritem); + item.topLeft.ry() += blockPos.y(); + + const QSize sizeHint = item.size; + + if (d->hasGrid()) { + const QSize sizeGrid = gridSize(); + const QSize resultingSize = sizeHint.boundedTo(sizeGrid); + QRect res(item.topLeft.x() + ((sizeGrid.width() - resultingSize.width()) / 2), + item.topLeft.y(), resultingSize.width(), resultingSize.height()); + //NOTE: think of using isRowHidden and setRowHidden methods + if (block.collapsed) { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft(-resultingSize.width()); + res.setHeight(0); + } + return d->mapToViewport(res); } - else - { - updateGeometries(); + + QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height()); + //NOTE: think of using isRowHidden and setRowHidden methods + if (block.collapsed) { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft(-sizeHint.width()); + res.setHeight(0); } + return d->mapToViewport(res); } -QModelIndex KCategorizedView::indexAt(const QPoint &point) const +KCategoryDrawer *KCategorizedView::categoryDrawer() const { - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - return QListView::indexAt(point); - } + return d->categoryDrawer; +} - QModelIndex index; +void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer) +{ + if (d->categoryDrawerV2) { + disconnect(d->categoryDrawerV2, SIGNAL(collapseOrExpandClicked(QModelIndex)), + this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex))); + } - const QModelIndexList item = d->intersectionSet(QRect(point, point)); + d->categoryDrawer = categoryDrawer; + d->categoryDrawerV2 = dynamic_cast(categoryDrawer); - if (item.count() == 1) - { - index = item[0]; + if (d->categoryDrawerV2) { + connect(d->categoryDrawerV2, SIGNAL(collapseOrExpandClicked(QModelIndex)), + this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex))); } - - return index; } -void KCategorizedView::reset() +int KCategorizedView::categorySpacing() const { - QListView::reset(); - - d->lastSelection = QItemSelection(); - d->forcedSelectionPosition = 0; - d->elementsInfo.clear(); - d->elementsPosition.clear(); - d->categoriesIndexes.clear(); - d->categoriesPosition.clear(); - d->categories.clear(); - d->intersectedIndexes.clear(); - d->modelIndexList.clear(); - d->hovered = QModelIndex(); - d->biggestItemSize = QSize(0, 0); - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; + return d->categorySpacing; } -void KCategorizedView::paintEvent(QPaintEvent *event) +void KCategorizedView::setCategorySpacing(int categorySpacing) { - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - QListView::paintEvent(event); + if (d->categorySpacing == categorySpacing) { return; } - bool alternatingRows = alternatingRowColors(); - - QStyleOptionViewItemV4 option = viewOptions(); - option.widget = this; - if (wordWrap()) - { - option.features |= QStyleOptionViewItemV4::WrapText; - } - - QPainter painter(viewport()); - QRect area = event->rect(); - const bool focus = (hasFocus() || viewport()->hasFocus()) && - currentIndex().isValid(); - const QStyle::State state = option.state; - const bool enabled = (state & QStyle::State_Enabled) != 0; + d->categorySpacing = categorySpacing; - painter.save(); - - QModelIndexList dirtyIndexes = d->intersectionSet(area); - bool alternate = false; - if (dirtyIndexes.count()) - { - alternate = dirtyIndexes[0].row() % 2; + for (QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) { + Private::Block &block = *it; + block.outOfQuarantine = false; } - foreach (const QModelIndex &index, dirtyIndexes) - { - if (alternatingRows && alternate) - { - option.features |= QStyleOptionViewItemV4::Alternate; - alternate = false; - } - else if (alternatingRows) - { - option.features &= ~QStyleOptionViewItemV4::Alternate; - alternate = true; - } - option.state = state; - option.rect = visualRect(index); +} - if (selectionModel() && selectionModel()->isSelected(index)) - { - option.state |= QStyle::State_Selected; - } +bool KCategorizedView::alternatingBlockColors() const +{ + return d->alternatingBlockColors; +} - if (enabled) - { - QPalette::ColorGroup cg; - if ((d->proxyModel->flags(index) & Qt::ItemIsEnabled) == 0) - { - option.state &= ~QStyle::State_Enabled; - cg = QPalette::Disabled; - } - else - { - cg = QPalette::Normal; - } - option.palette.setCurrentColorGroup(cg); - } +void KCategorizedView::setAlternatingBlockColors(bool enable) +{ + d->alternatingBlockColors = enable; +} - if (focus && currentIndex() == index) - { - option.state |= QStyle::State_HasFocus; - if (this->state() == EditingState) - option.state |= QStyle::State_Editing; - } +bool KCategorizedView::collapsibleBlocks() const +{ + return d->collapsibleBlocks; +} - if (index == d->hovered) - option.state |= QStyle::State_MouseOver; - else - option.state &= ~QStyle::State_MouseOver; +void KCategorizedView::setCollapsibleBlocks(bool enable) +{ + d->collapsibleBlocks = enable; +} - itemDelegate(index)->paint(&painter, option, index); +QModelIndex KCategorizedView::indexAt(const QPoint &point) const +{ + if (!d->isCategorized()) { + return QListView::indexAt(point); } - // Redraw categories - QStyleOptionViewItemV4 otherOption; - bool intersectedInThePast = false; - foreach (const QString &category, d->categories) - { - otherOption = option; - otherOption.rect = d->categoryVisualRect(category); - otherOption.state &= ~QStyle::State_MouseOver; - - if (otherOption.rect.intersects(area)) - { - intersectedInThePast = true; - - QModelIndex indexToDraw = d->proxyModel->index(d->categoriesIndexes[category][0].row(), d->proxyModel->sortColumn()); + const int rowCount = d->proxyModel->rowCount(); + if (!rowCount) { + return QModelIndex(); + } - d->drawNewCategory(indexToDraw, - d->proxyModel->sortRole(), otherOption, &painter); + // Binary search that will try to spot if there is an index under point + int bottom = 0; + int top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = d->proxyModel->index(middle, modelColumn(), rootIndex()); + QRect rect = visualRect(index); + const int verticalOff = verticalOffset(); + const int horizontalOff = horizontalOffset(); + rect.topLeft().ry() += verticalOff; + rect.topLeft().rx() += horizontalOff; + rect.bottomRight().ry() += verticalOff; + rect.bottomRight().rx() += horizontalOff; + if (rect.contains(point)) { + return index; } - else if (intersectedInThePast) - { - break; // the visible area has been finished, we don't need to keep asking, the rest won't intersect - // this is doable because we know that categories are correctly ordered on the list + if (point.y() > rect.bottomRight().y() || + (point.y() > rect.topLeft().y() && point.y() < rect.bottomRight().y() && + point.x() > rect.bottomRight().x())) { + bottom = middle + 1; + } else { + top = middle - 1; } } - if ((selectionMode() != SingleSelection) && (selectionMode() != NoSelection)) - { - if (d->mouseButtonPressed && !d->isDragging) - { - QPoint start, end, initialPressPosition; + return QModelIndex(); +} - initialPressPosition = d->initialPressPosition; +void KCategorizedView::reset() +{ + d->blocks.clear(); + QListView::reset(); +} - initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); +void KCategorizedView::paintEvent(QPaintEvent *event) +{ + if (!d->isCategorized()) { + QListView::paintEvent(event); + return; + } - if (d->initialPressPosition.x() > d->mousePosition.x() || - d->initialPressPosition.y() > d->mousePosition.y()) - { - start = d->mousePosition; - end = initialPressPosition; - } - else - { - start = initialPressPosition; - end = d->mousePosition; + const QPair intersecting = d->intersectingIndexesWithRect(viewport()->rect().intersected(event->rect())); + + QPainter p(viewport()); + p.save(); + + Q_ASSERT(selectionModel()->model() == d->proxyModel); + + //BEGIN: draw categories + QHash::ConstIterator it(d->blocks.constBegin()); + while (it != d->blocks.constEnd()) { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + QStyleOptionViewItemV4 option(viewOptions()); + option.features |= d->alternatingBlockColors && block.alternate ? QStyleOptionViewItemV4::Alternate + : QStyleOptionViewItemV4::None; + option.state |= !d->collapsibleBlocks || !block.collapsed ? QStyle::State_Open + : QStyle::State_None; + const int height = d->categoryDrawer->categoryHeight(categoryIndex, option); + QPoint pos = d->blockPosition(it.key()); + pos.ry() -= height; + option.rect.setTopLeft(pos); + option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin()); + option.rect.setHeight(height + d->blockHeight(it.key())); + option.rect = d->mapToViewport(option.rect); + if (!option.rect.intersects(viewport()->rect())) { + ++it; + continue; + } + d->categoryDrawer->drawCategory(categoryIndex, d->proxyModel->sortRole(), option, &p); + ++it; + } + //END: draw categories + + if (intersecting.first.isValid() && intersecting.second.isValid()) { + //BEGIN: draw items + int i = intersecting.first.row(); + int indexToCheckIfBlockCollapsed = i; + while (i <= intersecting.second.row()) { + //BEGIN: first check if the block is collapsed. if so, we have to skip the item painting + if (i == indexToCheckIfBlockCollapsed) { + const QModelIndex categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex()); + const QString category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + const Private::Block &block = d->blocks[category]; + indexToCheckIfBlockCollapsed = block.firstIndex.row() + block.items.count(); + if (block.collapsed) { + i = indexToCheckIfBlockCollapsed; + continue; + } } - - QStyleOptionRubberBand yetAnotherOption; - yetAnotherOption.initFrom(this); - yetAnotherOption.shape = QRubberBand::Rectangle; - yetAnotherOption.opaque = false; - yetAnotherOption.rect = QRect(start, end).intersected(viewport()->rect().adjusted(-16, -16, 16, 16)); - painter.save(); - style()->drawControl(QStyle::CE_RubberBand, &yetAnotherOption, &painter); - painter.restore(); + //END: first check if the block is collapsed. if so, we have to skip the item painting + + const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex()); + QStyleOptionViewItemV4 option(viewOptions()); + option.rect = visualRect(index); + option.widget = this; + option.features |= wordWrap() ? QStyleOptionViewItemV2::WrapText + : QStyleOptionViewItemV2::None; + option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver + : QStyle::State_None; + option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected + : QStyle::State_None; + option.state |= (index == currentIndex()) ? QStyle::State_HasFocus + : QStyle::State_None; + itemDelegate(index)->paint(&p, option, index); + ++i; } + //END: draw items } - if (d->isDragging && !d->dragLeftViewport) - { - painter.setOpacity(0.5); - d->drawDraggedItems(&painter); + //BEGIN: draw selection rect + if (isSelectionRectVisible() && d->rubberBandRect.isValid()) { + QStyleOptionRubberBand opt; + opt.initFrom(this); + opt.shape = QRubberBand::Rectangle; + opt.opaque = false; + opt.rect = d->mapToViewport(d->rubberBandRect).intersected(viewport()->rect().adjusted(-16, -16, 16, 16)); + p.save(); + style()->drawControl(QStyle::CE_RubberBand, &opt, &p); + p.restore(); } + //END: draw selection rect - painter.restore(); + p.restore(); } void KCategorizedView::resizeEvent(QResizeEvent *event) { + d->regenerateAllElements(); QListView::resizeEvent(event); - - // Clear the items positions cache - d->elementsPosition.clear(); - d->categoriesPosition.clear(); - d->forcedSelectionPosition = 0; - - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - return; - } - - d->updateScrollbars(); } void KCategorizedView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) { - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { + if (!d->isCategorized()) { QListView::setSelection(rect, flags); return; } - if (!flags) + if (rect.topLeft() == rect.bottomRight()) { + const QModelIndex index = indexAt(rect.topLeft()); + selectionModel()->select(index, flags); return; - - if (flags & QItemSelectionModel::Clear) - { - selectionModel()->clear(); - d->lastSelection.clear(); } - QModelIndexList dirtyIndexes = d->intersectionSet(rect); + const QPair intersecting = d->intersectingIndexesWithRect(rect); - // no items affected, just leave - if (!dirtyIndexes.count()) - { - selectionModel()->select(d->lastSelection, QItemSelectionModel::SelectCurrent); + QItemSelection selection; - return; - } - - QModelIndex topLeft; - QModelIndex bottomRight; - - if (d->mouseButtonPressed || d->rightMouseButtonPressed) // selection with click + drag - { - QItemSelection selection; - - QModelIndex prev = dirtyIndexes[0]; - QModelIndex first = prev; - foreach (const QModelIndex &index, dirtyIndexes) - { - // we have a different interval. non-contiguous items - if ((index.row() - prev.row()) > 1) { - selection << QItemSelectionRange(first, prev); - - first = index; - } - - prev = index; - } - - selection << QItemSelectionRange(first, prev); - - if (flags & QItemSelectionModel::Current) - { - if (rect.topLeft() == rect.bottomRight()) - { - selectionModel()->setCurrentIndex(indexAt(rect.topLeft()), QItemSelectionModel::NoUpdate); + //TODO: think of a faster implementation + QModelIndex firstIndex; + QModelIndex lastIndex; + for (int i = intersecting.first.row(); i <= intersecting.second.row(); ++i) { + const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex()); + const bool visualRectIntersects = visualRect(index).intersects(rect); + if (firstIndex.isValid()) { + if (visualRectIntersects) { + lastIndex = index; + } else { + selection << QItemSelectionRange(firstIndex, lastIndex); + firstIndex = QModelIndex(); } - - selection.merge(d->lastSelection, flags); - } - else - { - selection.merge(selectionModel()->selection(), flags); - - selectionModel()->select(selection, QItemSelectionModel::SelectCurrent); - - return; + } else if (visualRectIntersects) { + firstIndex = index; + lastIndex = index; } - - selectionModel()->select(selection, flags); } - else // selection with click + keyboard keys - { - QModelIndex topLeftIndex = indexAt(QPoint(rect.topLeft().x(), - rect.topLeft().y())); - QModelIndex bottomRightIndex = indexAt(QPoint(rect.bottomRight().x(), - rect.bottomRight().y())); - - // keyboard selection comes "upside down". Let's normalize it - if (topLeftIndex.row() > bottomRightIndex.row()) - { - QModelIndex auxIndex = topLeftIndex; - topLeftIndex = bottomRightIndex; - bottomRightIndex = auxIndex; - } - - int viewportWidth = viewport()->width() - spacing(); - int itemWidth; - - if (gridSize().isEmpty()) - { - itemWidth = d->biggestItemSize.width(); - } - else - { - itemWidth = gridSize().width(); - } - - int itemWidthPlusSeparation = spacing() + itemWidth; - if (!itemWidthPlusSeparation) - itemWidthPlusSeparation++; - int elementsPerRow = viewportWidth / itemWidthPlusSeparation; - if (!elementsPerRow) - elementsPerRow++; - - QModelIndexList theoricDirty(dirtyIndexes); - dirtyIndexes.clear(); - int first = model()->rowCount(); - int last = 0; - - foreach (const QModelIndex &index, theoricDirty) - { - if ((index.row() < first) && - ((((topLeftIndex.row() / elementsPerRow) == (index.row() / elementsPerRow)) && - ((topLeftIndex.row() % elementsPerRow) <= (index.row() % elementsPerRow))) || - (topLeftIndex.row() / elementsPerRow) != (index.row() / elementsPerRow))) - { - first = index.row(); - topLeft = index; - } - - if ((index.row() > last) && - ((((bottomRightIndex.row() / elementsPerRow) == (index.row() / elementsPerRow)) && - ((bottomRightIndex.row() % elementsPerRow) >= (index.row() % elementsPerRow))) || - (bottomRightIndex.row() / elementsPerRow) != (index.row() / elementsPerRow))) - { - last = index.row(); - bottomRight = index; - } - } - for (int i = first; i <= last; i++) - { - dirtyIndexes << model()->index(i, theoricDirty[0].column(), theoricDirty[0].parent()); - } - - QItemSelection selection(topLeft, bottomRight); - - selectionModel()->select(selection, flags); + if (firstIndex.isValid()) { + selection << QItemSelectionRange(firstIndex, lastIndex); } + + selectionModel()->select(selection, flags); } void KCategorizedView::mouseMoveEvent(QMouseEvent *event) { QListView::mouseMoveEvent(event); - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - return; - } - - const QModelIndexList item = d->intersectionSet(QRect(event->pos(), event->pos())); - - if (item.count() == 1) - { - d->hovered = item[0]; - } - else - { - d->hovered = QModelIndex(); - } - - const QString previousHoveredCategory = d->hoveredCategory; - - d->mousePosition = event->pos(); - d->hoveredCategory.clear(); - - // Redraw categories - foreach (const QString &category, d->categories) - { - if (d->categoryVisualRect(category).intersects(QRect(event->pos(), event->pos()))) - { - d->hoveredCategory = category; - viewport()->update(d->categoryVisualRect(category)); - } - else if ((category == previousHoveredCategory) && - (!d->categoryVisualRect(previousHoveredCategory).intersects(QRect(event->pos(), event->pos())))) - { - viewport()->update(d->categoryVisualRect(category)); - } - } + d->hoveredIndex = indexAt(event->pos()); - QRect rect; - if (d->mouseButtonPressed && !d->isDragging) - { - QPoint start, end, initialPressPosition; - - initialPressPosition = d->initialPressPosition; - - initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); - - if (d->initialPressPosition.x() > d->mousePosition.x() || - d->initialPressPosition.y() > d->mousePosition.y()) - { - start = d->mousePosition; - end = initialPressPosition; - } - else - { - start = initialPressPosition; - end = d->mousePosition; - } + const SelectionMode itemViewSelectionMode = selectionMode(); - rect = QRect(start, end).adjusted(-16, -16, 16, 16); - rect = rect.united(QRect(start, end).adjusted(16, 16, -16, -16)).intersected(viewport()->rect()); - - viewport()->update(rect); + if (state() == DragSelectingState && isSelectionRectVisible() && itemViewSelectionMode != SingleSelection + && itemViewSelectionMode != NoSelection) { + QRect rect(d->pressedPosition, event->pos() + QPoint(horizontalOffset(), verticalOffset())); + rect = rect.normalized(); + update(rect.united(d->rubberBandRect)); + d->rubberBandRect = rect; } } void KCategorizedView::mousePressEvent(QMouseEvent *event) { - d->dragLeftViewport = false; - - if (event->button() == Qt::LeftButton) - { - d->mouseButtonPressed = true; - - d->initialPressPosition = event->pos(); - d->initialPressPosition.setY(d->initialPressPosition.y() + - verticalOffset()); - d->initialPressPosition.setX(d->initialPressPosition.x() + - horizontalOffset()); - } - else if (event->button() == Qt::RightButton) - { - d->rightMouseButtonPressed = true; - } - QListView::mousePressEvent(event); - - if (selectionModel()) - { - d->lastSelection = selectionModel()->selection(); + if (event->button() == Qt::LeftButton) { + d->pressedPosition = event->pos(); + d->pressedPosition.rx() += horizontalOffset(); + d->pressedPosition.ry() += verticalOffset(); } - - viewport()->update(d->categoryVisualRect(d->hoveredCategory)); } void KCategorizedView::mouseReleaseEvent(QMouseEvent *event) { - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - QListView::mouseReleaseEvent(event); - - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - return; - } - - QPoint initialPressPosition = viewport()->mapFromGlobal(QCursor::pos()); - initialPressPosition.setY(initialPressPosition.y() + verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() + horizontalOffset()); - - if ((selectionMode() != SingleSelection) && (selectionMode() != NoSelection) && - (initialPressPosition == d->initialPressPosition)) - { - foreach(const QString &category, d->categories) - { - if (d->categoryVisualRect(category).contains(event->pos()) && - selectionModel()) - { - QItemSelection selection = selectionModel()->selection(); - QModelIndexList indexList = d->categoriesIndexes[category]; - - foreach (const QModelIndex &index, indexList) - { - QModelIndex selectIndex = index.model()->index(index.row(), 0); - - selection << QItemSelectionRange(selectIndex); - } - - selectionModel()->select(selection, QItemSelectionModel::SelectCurrent); - - break; - } - } - } - - QRect rect; - if (!d->isDragging) - { - QPoint start, end, initialPressPosition; - - initialPressPosition = d->initialPressPosition; - - initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); - - if (d->initialPressPosition.x() > d->mousePosition.x() || - d->initialPressPosition.y() > d->mousePosition.y()) - { - start = d->mousePosition; - end = initialPressPosition; - } - else - { - start = initialPressPosition; - end = d->mousePosition; - } - - rect = QRect(start, end).adjusted(-16, -16, 16, 16); - rect = rect.united(QRect(start, end).adjusted(16, 16, -16, -16)).intersected(viewport()->rect()); - - viewport()->update(rect); - } - - if (d->hovered.isValid()) - viewport()->update(visualRect(d->hovered)); - else if (!d->hoveredCategory.isEmpty()) - viewport()->update(d->categoryVisualRect(d->hoveredCategory)); + d->pressedPosition = QPoint(); + d->rubberBandRect = QRect(); } void KCategorizedView::leaveEvent(QEvent *event) { - d->hovered = QModelIndex(); - d->hoveredCategory.clear(); - QListView::leaveEvent(event); + + if (d->hoveredIndex.isValid()) { + viewport()->update(visualRect(d->hoveredIndex)); + d->hoveredIndex = QModelIndex(); + } } void KCategorizedView::startDrag(Qt::DropActions supportedActions) { - // FIXME: QAbstractItemView does far better here since it sets the - // pixmap of selected icons to the dragging cursor, but it sets a non - // ARGB window so it is no transparent. Use QAbstractItemView when - // this is fixed on Qt. - // QAbstractItemView::startDrag(supportedActions); -#if defined(DOLPHIN_DRAGANDDROP) - Q_UNUSED(supportedActions); -#else QListView::startDrag(supportedActions); -#endif - - d->isDragging = false; - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - - viewport()->update(d->lastDraggedItemsRect); } void KCategorizedView::dragMoveEvent(QDragMoveEvent *event) { - d->mousePosition = event->pos(); - - if (d->mouseButtonPressed) - { - d->isDragging = true; - } - else - { - d->isDragging = false; - } - - d->dragLeftViewport = false; - -#if defined(DOLPHIN_DRAGANDDROP) QAbstractItemView::dragMoveEvent(event); -#else - QListView::dragMoveEvent(event); -#endif - - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - return; - } - - d->hovered = indexAt(event->pos()); - -#if !defined(DOLPHIN_DRAGANDDROP) - d->drawDraggedItems(); -#endif } void KCategorizedView::dragLeaveEvent(QDragLeaveEvent *event) { - d->dragLeftViewport = true; - -#if defined(DOLPHIN_DRAGANDDROP) QAbstractItemView::dragLeaveEvent(event); -#else - QListView::dragLeaveEvent(event); -#endif } void KCategorizedView::dropEvent(QDropEvent *event) { -#if defined(DOLPHIN_DRAGANDDROP) QAbstractItemView::dropEvent(event); -#else - QListView::dropEvent(event); -#endif } +//TODO: improve so we take into account isRowHidden in a better way +//TODO: improve se we take into account collapsed blocks QModelIndex KCategorizedView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { - if ((viewMode() != KCategorizedView::IconMode) || - !d->proxyModel || - !d->categoryDrawer || - d->categories.isEmpty() || - !d->proxyModel->isCategorizedModel()) - { + if (!d->isCategorized()) { return QListView::moveCursor(cursorAction, modifiers); } - int viewportWidth = viewport()->width() - spacing(); - int itemWidth; - - if (gridSize().isEmpty()) - { - itemWidth = d->biggestItemSize.width(); - } - else - { - itemWidth = gridSize().width(); - } - - int itemWidthPlusSeparation = spacing() + itemWidth; - if (!itemWidthPlusSeparation) - itemWidthPlusSeparation++; - int elementsPerRow = viewportWidth / itemWidthPlusSeparation; - if (!elementsPerRow) - elementsPerRow++; - - QModelIndex current = selectionModel() ? selectionModel()->currentIndex() - : QModelIndex(); - - if (!current.isValid()) - { - if (cursorAction == MoveEnd) - { - current = model()->index(model()->rowCount() - 1, 0, QModelIndex()); - d->forcedSelectionPosition = d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow; + const QModelIndex current = currentIndex(); + const QRect currentRect = visualRect(current); + if (!current.isValid()) { + const int rowCount = d->proxyModel->rowCount(rootIndex()); + if (!rowCount) { + return QModelIndex(); } - else - { - current = model()->index(0, 0, QModelIndex()); - d->forcedSelectionPosition = 0; + int row = 0; + while (row < rowCount && isRowHidden(row)) { + ++row; } - - return current; + if (row >= rowCount) { + return QModelIndex(); + } + return d->proxyModel->index(row, modelColumn(), rootIndex()); } - QString lastCategory = d->categories.first(); - QString theCategory = d->categories.first(); - QString afterCategory = d->categories.first(); - - bool hasToBreak = false; - foreach (const QString &category, d->categories) - { - if (hasToBreak) - { - afterCategory = category; - + switch (cursorAction) { + case MoveLeft: { + if (!current.row()) { + return QModelIndex(); + } + const QModelIndex previous = d->proxyModel->index(current.row() - 1, modelColumn(), rootIndex()); + const QRect previousRect = visualRect(previous); + if (previousRect.top() == currentRect.top()) { + return previous; + } + return QModelIndex(); + } + case MoveRight: { + if (current.row() == d->proxyModel->rowCount() - 1) { + return QModelIndex(); + } + const QModelIndex next = d->proxyModel->index(current.row() + 1, modelColumn(), rootIndex()); + const QRect nextRect = visualRect(next); + if (nextRect.top() == currentRect.top()) { + return next; + } + return QModelIndex(); + } + case MoveDown: { + if (d->hasGrid()) { + const int maxItemsPerRow = qMax(d->viewportWidth() / gridSize().width(), 1); + if (current.row() + maxItemsPerRow > d->proxyModel->rowCount()) { + return QModelIndex(); + } + + } + } + default: break; - } + } - if (category == d->elementsInfo[current.row()].category) - { - theCategory = category; + return QModelIndex(); +} - hasToBreak = true; - } +void KCategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent, + int start, + int end) +{ + if (!d->isCategorized()) { + QListView::rowsAboutToBeRemoved(parent, start, end); + return; + } - if (!hasToBreak) - { - lastCategory = category; - } + if (end - start + 1 == d->proxyModel->rowCount()) { + d->blocks.clear(); + QListView::rowsAboutToBeRemoved(parent, start, end); + return; } - switch (cursorAction) - { - case QAbstractItemView::MoveUp: { - if (d->elementsInfo[current.row()].relativeOffsetToCategory >= elementsPerRow) - { - int indexToMove = current.row(); - indexToMove -= qMin(((d->elementsInfo[current.row()].relativeOffsetToCategory) + d->forcedSelectionPosition), elementsPerRow - d->forcedSelectionPosition + (d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)); + // Removal feels a bit more complicated than insertion. Basically we can consider there are + // 3 different cases when going to remove items. (*) represents an item, Items between ([) and + // (]) are the ones which are marked for removal. + // + // - 1st case: + // ... * * * * * * [ * * * ... + // + // The items marked for removal are the last part of this category. No need to mark any item + // of this category as in quarantine, because no special offset will be pushed to items at + // the right because of any changes (since the removed items are those on the right most part + // of the category). + // + // - 2nd case: + // ... * * * * * * ] * * * ... + // + // The items marked for removal are the first part of this category. We have to mark as in + // quarantine all items in this category. Absolutely all. All items will have to be moved to + // the left (or moving up, because rows got a different offset). + // + // - 3rd case: + // ... * * [ * * * * ] * * ... + // + // The items marked for removal are in between of this category. We have to mark as in + // quarantine only those items that are at the right of the end of the removal interval, + // (starting on "]"). + // + // It hasn't been explicitly said, but when we remove, we have to mark all blocks that are + // located under the top most affected category as in quarantine (the block itself, as a whole), + // because such a change can force it to have a different offset (note that items themselves + // contain relative positions to the block, so marking the block as in quarantine is enough). + // + // Also note that removal implicitly means that we have to update correctly firstIndex of each + // block, and in general keep updated the internal information of elements. + + QStringList listOfCategoriesMarkedForRemoval; + + QString lastCategory; + int alreadyRemoved = 0; + for (int i = start; i <= end; ++i) { + const QModelIndex index = d->proxyModel->index(i, modelColumn(), parent); + + Q_ASSERT(index.isValid()); + + const QString category = d->categoryForIndex(index); + + if (lastCategory != category) { + lastCategory = category; + alreadyRemoved = 0; + } - return d->proxyModel->index(indexToMove, 0); - } - else - { - int lastCategoryLastRow = (d->categoriesIndexes[lastCategory].count() - 1) % elementsPerRow; - int indexToMove = current.row() - d->elementsInfo[current.row()].relativeOffsetToCategory; - - if (d->forcedSelectionPosition >= lastCategoryLastRow) - { - indexToMove -= 1; - } - else - { - indexToMove -= qMin((lastCategoryLastRow - d->forcedSelectionPosition + 1), d->forcedSelectionPosition + elementsPerRow + 1); - } + Private::Block &block = d->blocks[category]; + block.items.removeAt(i - block.firstIndex.row() - alreadyRemoved); + ++alreadyRemoved; - return d->proxyModel->index(indexToMove, 0); - } + if (!block.items.count()) { + listOfCategoriesMarkedForRemoval << category; } - case QAbstractItemView::MoveDown: { - if (d->elementsInfo[current.row()].relativeOffsetToCategory < (d->categoriesIndexes[theCategory].count() - 1 - ((d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow))) - { - int indexToMove = current.row(); - indexToMove += qMin(elementsPerRow, d->categoriesIndexes[theCategory].count() - 1 - d->elementsInfo[current.row()].relativeOffsetToCategory); + //TODO: assign less times + block.height = -1; - return d->proxyModel->index(indexToMove, 0); - } - else - { - int afterCategoryLastRow = qMin(elementsPerRow, d->categoriesIndexes[afterCategory].count()); - int indexToMove = current.row() + (d->categoriesIndexes[theCategory].count() - d->elementsInfo[current.row()].relativeOffsetToCategory); - - if (d->forcedSelectionPosition >= afterCategoryLastRow) - { - indexToMove += afterCategoryLastRow - 1; - } - else - { - indexToMove += qMin(d->forcedSelectionPosition, elementsPerRow); - } + viewport()->update(); + } - return d->proxyModel->index(indexToMove, 0); - } + //BEGIN: update the items that are in quarantine in affected categories + { + const QModelIndex lastIndex = d->proxyModel->index(end, modelColumn(), parent); + const QString category = d->categoryForIndex(lastIndex); + Private::Block &block = d->blocks[category]; + if (block.items.count() && start <= block.firstIndex.row() && end >= block.firstIndex.row()) { + block.firstIndex = d->proxyModel->index(end + 1, modelColumn(), parent); } + block.quarantineStart = block.firstIndex; + } + //END: update the items that are in quarantine in affected categories - case QAbstractItemView::MoveLeft: - if (layoutDirection() == Qt::RightToLeft) - { - if (!(d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow)) - return current; - - d->forcedSelectionPosition = d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow; - -#if 0 //follow qt view behavior. lateral movements won't change visual row - if (d->forcedSelectionPosition < 0) - d->forcedSelectionPosition = (d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow; -#endif + Q_FOREACH (const QString &category, listOfCategoriesMarkedForRemoval) { + d->blocks.remove(category); + } - return d->proxyModel->index(current.row() + 1, 0); + //BEGIN: mark as in quarantine those categories that are under the affected ones + { + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = d->blocks.values(); + qSort(blockList.begin(), blockList.end(), Private::Block::lessThan); + QList firstIndexesRows; + foreach (const Private::Block &block, blockList) { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for (QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) { + Private::Block &block = *it; + if (block.firstIndex.row() > start) { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } else if (block.firstIndex.row() == start) { + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; } + } + } + //END: mark as in quarantine those categories that are under the affected ones - if (!(d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)) - return current; - - d->forcedSelectionPosition = d->elementsInfo[current.row() - 1].relativeOffsetToCategory % elementsPerRow; - -#if 0 //follow qt view behavior. lateral movements won't change visual row - if (d->forcedSelectionPosition < 0) - d->forcedSelectionPosition = (d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow; -#endif + QListView::rowsAboutToBeRemoved(parent, start, end); +} - return d->proxyModel->index(current.row() - 1, 0); +void KCategorizedView::updateGeometries() +{ + const int verticalOff = verticalOffset(); - case QAbstractItemView::MoveRight: - if (layoutDirection() == Qt::RightToLeft) - { - if (!(d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)) - return current; + QListView::updateGeometries(); - d->forcedSelectionPosition = d->elementsInfo[current.row() - 1].relativeOffsetToCategory % elementsPerRow; + if (!d->isCategorized()) { + return; + } -#if 0 //follow qt view behavior. lateral movements won't change visual row - if (d->forcedSelectionPosition < 0) - d->forcedSelectionPosition = (d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow; -#endif + const int rowCount = d->proxyModel->rowCount(); + if (!rowCount) { + verticalScrollBar()->setRange(0, 0); + return; + } - return d->proxyModel->index(current.row() - 1, 0); - } + const QModelIndex lastIndex = d->proxyModel->index(rowCount - 1, modelColumn(), rootIndex()); + Q_ASSERT(lastIndex.isValid()); + QRect lastItemRect = visualRect(lastIndex); + + if (d->hasGrid()) { + lastItemRect.setSize(lastItemRect.size().expandedTo(gridSize())); + } else { + if (uniformItemSizes()) { + QSize itemSize = sizeHintForIndex(lastIndex); + itemSize.setHeight(itemSize.height() + spacing()); + lastItemRect.setSize(itemSize); + } else { + QSize itemSize = sizeHintForIndex(lastIndex); + const QString category = d->categoryForIndex(lastIndex); + itemSize.setHeight(d->highestElementInLastRow(d->blocks[category]) + spacing()); + lastItemRect.setSize(itemSize); + } + } - if (!(d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow)) - return current; + const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height(); - d->forcedSelectionPosition = d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow; + verticalScrollBar()->setRange(0, bottomRange); -#if 0 //follow qt view behavior. lateral movements won't change visual row - if (d->forcedSelectionPosition < 0) - d->forcedSelectionPosition = (d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow; -#endif + verticalScrollBar()->setValue(verticalOff); - return d->proxyModel->index(current.row() + 1, 0); + //TODO: also consider working with the horizontal scroll bar. since at this level I am not still + // supporting "top to bottom" flow, there is no real problem. If I support that someday + // (think how to draw categories ! crazy stuff !), we would have to take care of the + // horizontal scroll bar too. In theory, as KCategorizedView has been designed, there is + // no need of horizontal scroll bar. + horizontalScrollBar()->setRange(0, 0); +} - default: - break; - } +void KCategorizedView::currentChanged(const QModelIndex ¤t, + const QModelIndex &previous) +{ + QListView::currentChanged(current, previous); +} - return QListView::moveCursor(cursorAction, modifiers); +void KCategorizedView::dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight) +{ + QListView::dataChanged(topLeft, bottomRight); } void KCategorizedView::rowsInserted(const QModelIndex &parent, @@ -1482,26 +1107,7 @@ void KCategorizedView::rowsInserted(const QModelIndex &parent, int end) { QListView::rowsInserted(parent, start, end); - - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - d->forcedSelectionPosition = 0; - d->elementsInfo.clear(); - d->elementsPosition.clear(); - d->categoriesIndexes.clear(); - d->categoriesPosition.clear(); - d->categories.clear(); - d->intersectedIndexes.clear(); - d->modelIndexList.clear(); - d->hovered = QModelIndex(); - d->biggestItemSize = QSize(0, 0); - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - - return; - } - - rowsInsertedArtifficial(parent, start, end); + d->rowsInserted(parent, start, end); } void KCategorizedView::rowsInsertedArtifficial(const QModelIndex &parent, @@ -1509,75 +1115,8 @@ void KCategorizedView::rowsInsertedArtifficial(const QModelIndex &parent, int end) { Q_UNUSED(parent); - - d->forcedSelectionPosition = 0; - d->elementsInfo.clear(); - d->elementsPosition.clear(); - d->categoriesIndexes.clear(); - d->categoriesPosition.clear(); - d->categories.clear(); - d->intersectedIndexes.clear(); - d->modelIndexList.clear(); - d->hovered = QModelIndex(); - d->biggestItemSize = QSize(0, 0); - d->mouseButtonPressed = false; - d->rightMouseButtonPressed = false; - - if (start > end || end < 0 || start < 0 || !d->proxyModel->rowCount()) - { - return; - } - - // Add all elements mapped to the source model and explore categories - QString prevCategory = d->proxyModel->data(d->proxyModel->index(0, d->proxyModel->sortColumn()), KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); - QString lastCategory = prevCategory; - QModelIndexList modelIndexList; - struct Private::ElementInfo elementInfo; - int offset = -1; - for (int k = 0; k < d->proxyModel->rowCount(); ++k) - { - QModelIndex index = d->proxyModel->index(k, d->proxyModel->sortColumn()); - QModelIndex indexSize = d->proxyModel->index(k, 0); - - d->biggestItemSize = QSize(qMax(sizeHintForIndex(indexSize).width(), - d->biggestItemSize.width()), - qMax(sizeHintForIndex(indexSize).height(), - d->biggestItemSize.height())); - - d->modelIndexList << index; - - lastCategory = d->proxyModel->data(index, KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); - - elementInfo.category = lastCategory; - - if (prevCategory != lastCategory) - { - offset = 0; - d->categoriesIndexes.insert(prevCategory, modelIndexList); - d->categories << prevCategory; - modelIndexList.clear(); - } - else - { - offset++; - } - - elementInfo.relativeOffsetToCategory = offset; - - modelIndexList << index; - prevCategory = lastCategory; - - d->elementsInfo.insert(index.row(), elementInfo); - } - - d->categoriesIndexes.insert(prevCategory, modelIndexList); - d->categories << prevCategory; - - d->updateScrollbars(); - - // FIXME: We need to safely save the last selection. This is on my TODO - // list (ereslibre). - selectionModel()->clear(); + Q_UNUSED(start); + Q_UNUSED(end); } void KCategorizedView::rowsRemoved(const QModelIndex &parent, @@ -1587,91 +1126,16 @@ void KCategorizedView::rowsRemoved(const QModelIndex &parent, Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); - if (d->proxyModel && d->categoryDrawer && d->proxyModel->isCategorizedModel()) - { - // Force the view to update all elements - rowsInsertedArtifficial(QModelIndex(), 0, d->proxyModel->rowCount() - 1); - } -} - -void KCategorizedView::updateGeometries() -{ - if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) - { - QListView::updateGeometries(); - return; - } - - // Avoid QListView::updateGeometries(), since it will try to set another - // range to our scroll bars, what we don't want (ereslibre) - QAbstractItemView::updateGeometries(); } void KCategorizedView::slotLayoutChanged() { - d->layoutChanged(); -} - -void KCategorizedView::currentChanged(const QModelIndex ¤t, - const QModelIndex &previous) -{ - // We need to update the forcedSelectionPosition property in order to correctly - // navigate after with keyboard using up & down keys - - int viewportWidth = viewport()->width() - spacing(); - - int itemHeight; - int itemWidth; - - if (gridSize().isEmpty()) - { - itemHeight = d->biggestItemSize.height(); - itemWidth = d->biggestItemSize.width(); - } - else - { - itemHeight = gridSize().height(); - itemWidth = gridSize().width(); + d->blocks.clear(); + if (d->proxyModel->rowCount()) { + d->rowsInserted(rootIndex(), 0, d->proxyModel->rowCount() - 1); } - - int itemWidthPlusSeparation = spacing() + itemWidth; - if (!itemWidthPlusSeparation) - itemWidthPlusSeparation++; - int elementsPerRow = viewportWidth / itemWidthPlusSeparation; - if (!elementsPerRow) - elementsPerRow++; - - if (d->mouseButtonPressed || d->rightMouseButtonPressed) - d->forcedSelectionPosition = d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow; - - QListView::currentChanged(current, previous); } -void KCategorizedView::dataChanged(const QModelIndex &topLeft, - const QModelIndex &bottomRight) -{ - if (topLeft == bottomRight) - { - d->cacheIndex(topLeft); - } - else - { - const int columnStart = topLeft.column(); - const int columnEnd = bottomRight.column(); - const int rowStart = topLeft.row(); - const int rowEnd = bottomRight.row(); - - for (int row = rowStart; row <= rowEnd; ++row) - { - for (int column = columnStart; column <= columnEnd; ++column) - { - d->cacheIndex(d->proxyModel->index(row, column)); - } - } - } - - QListView::dataChanged(topLeft, bottomRight); - slotLayoutChanged(); -} +//END: Public part #include "kcategorizedview.moc" diff --git a/kdeui/itemviews/kcategorizedview.h b/kdeui/itemviews/kcategorizedview.h index 9352a15..caba845 100644 --- a/kdeui/itemviews/kcategorizedview.h +++ b/kdeui/itemviews/kcategorizedview.h @@ -1,6 +1,6 @@ /** * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007, 2008 Rafael Fernández López * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -26,6 +26,7 @@ #include class KCategoryDrawer; +class KCategoryDrawerV2; /** * @short Item view for listing items @@ -57,9 +58,38 @@ public: void setCategoryDrawer(KCategoryDrawer *categoryDrawer); + /** + * @since 4.3 + */ + int categorySpacing() const; + + /** + * @since 4.3 + */ + void setCategorySpacing(int categorySpacing); + + /** + * @since 4.3 + */ + bool alternatingBlockColors() const; + + /** + * @since 4.3 + */ + void setAlternatingBlockColors(bool enable); + + /** + * @since 4.3 + */ + bool collapsibleBlocks() const; + + /** + * @since 4.3 + */ + void setCollapsibleBlocks(bool enable); + virtual QModelIndex indexAt(const QPoint &point) const; -public Q_SLOTS: virtual void reset(); protected: @@ -89,11 +119,23 @@ protected: virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers); -protected Q_SLOTS: + virtual void rowsAboutToBeRemoved(const QModelIndex &parent, + int start, + int end); + + virtual void updateGeometries(); + + virtual void currentChanged(const QModelIndex ¤t, + const QModelIndex &previous); + + virtual void dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight); + virtual void rowsInserted(const QModelIndex &parent, int start, int end); +protected Q_SLOTS: virtual void rowsInsertedArtifficial(const QModelIndex &parent, int start, int end); @@ -102,19 +144,13 @@ protected Q_SLOTS: int start, int end); - virtual void updateGeometries(); - virtual void slotLayoutChanged(); - virtual void currentChanged(const QModelIndex ¤t, - const QModelIndex &previous); - - virtual void dataChanged(const QModelIndex &topLeft, - const QModelIndex &bottomRight); - private: class Private; Private *const d; + + Q_PRIVATE_SLOT(d, void _k_slotCollapseOrExpandClicked()) }; #endif // KCATEGORIZEDVIEW_H diff --git a/kdeui/itemviews/kcategorizedview_p.h b/kdeui/itemviews/kcategorizedview_p.h index a18854b..9e7210f 100644 --- a/kdeui/itemviews/kcategorizedview_p.h +++ b/kdeui/itemviews/kcategorizedview_p.h @@ -1,6 +1,6 @@ /** * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007, 2008 Rafael Fernández López * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -30,135 +30,98 @@ class KCategoryDrawer; class KCategorizedView::Private { public: - Private(KCategorizedView *listView); - ~Private(); - + struct Block; + struct Item; - // Methods - - /** - * Returns the list of items that intersects with @p rect - */ - const QModelIndexList &intersectionSet(const QRect &rect); + Private(KCategorizedView *q); + ~Private(); - /** - * Gets the item rect in the viewport for @p index - */ - QRect visualRectInViewport(const QModelIndex &index) const; + bool isCategorized() const; /** - * Returns the category rect in the viewport for @p category + * Returns the first and last element that intersects with rect. + * + * @note see that here we cannot take out items between first and last (as we could + * do with the rubberband). + * + * Complexity: O(log(n)) where n is model()->rowCount(). */ - QRect visualCategoryRectInViewport(const QString &category) const; + QPair intersectingIndexesWithRect(const QRect &rect) const; /** - * Caches and returns the rect that corresponds to @p index + * Returns the position of the block of @p category. + * + * Complexity: O(n) where n is the number of different categories when the block has been + * marked as in quarantine. O(1) the rest of the times (the vast majority). */ - const QRect &cacheIndex(const QModelIndex &index); + QPoint blockPosition(const QString &category); /** - * Caches and returns the rect that corresponds to @p category + * Returns the height of the block determined by @p category. */ - const QRect &cacheCategory(const QString &category); + int blockHeight(const QString &category); /** - * Returns the rect that corresponds to @p index - * @note If the rect is not cached, it becomes cached + * Returns the actual viewport width. */ - const QRect &cachedRectIndex(const QModelIndex &index); + int viewportWidth() const; /** - * Returns the rect that corresponds to @p category - * @note If the rect is not cached, it becomes cached + * Marks all elements as in quarantine. + * + * Complexity: O(n) where n is model()->rowCount(). + * + * @warning this is an expensive operation */ - const QRect &cachedRectCategory(const QString &category); + void regenerateAllElements(); /** - * Returns the visual rect (taking in count x and y offsets) for @p index - * @note If the rect is not cached, it becomes cached + * Update internal information, and keep sync with the real information that the model contains. */ - QRect visualRect(const QModelIndex &index); + void rowsInserted(const QModelIndex &parent, int start, int end); /** - * Returns the visual rect (taking in count x and y offsets) for @p category - * @note If the rect is not cached, it becomes cached + * Returns @p rect in viewport terms, taking in count horizontal and vertical offsets. */ - QRect categoryVisualRect(const QString &category); + QRect mapToViewport(const QRect &rect) const; /** - * This method will draw a new category represented by index - * @param index on the rect specified by @p option.rect, with - * painter @p painter + * Returns @p rect in absolute terms, converted from viewport position. */ - void drawNewCategory(const QModelIndex &index, - int sortRole, - const QStyleOption &option, - QPainter *painter); + QRect mapFromViewport(const QRect &rect) const; /** - * This method will update scrollbars ranges. Called when our model changes - * or when the view is resized + * Returns the height of the highest element in last row. This is only applicable if there is + * no grid set and uniformItemSizes is false. + * + * @param block in which block are we searching. Necessary to stop the search if we hit the + * first item in this block. */ - void updateScrollbars(); + int highestElementInLastRow(const Block &block) const; /** - * This method will draw dragged items in the painting operation + * Returns whether the view has a valid grid size. */ - void drawDraggedItems(QPainter *painter); + bool hasGrid() const; - /** - * This method will determine which rect needs to be updated because of a - * dragging operation - */ - void drawDraggedItems(); + QString categoryForIndex(const QModelIndex &index) const; - void layoutChanged(bool forceItemReload = false); + void _k_slotCollapseOrExpandClicked(); + KCategorizedView *q; + KCategorizedSortFilterProxyModel *proxyModel; + KCategoryDrawer *categoryDrawer; + KCategoryDrawerV2 *categoryDrawerV2; + int categorySpacing; + bool alternatingBlockColors; + bool collapsibleBlocks; - // Attributes + QModelIndex hoveredIndex; - struct ElementInfo - { - QString category; - int relativeOffsetToCategory; - }; + QPoint pressedPosition; + QRect rubberBandRect; - // Basic data - KCategorizedView *listView; - KCategoryDrawer *categoryDrawer; - QSize biggestItemSize; - - // Behavior data - bool mouseButtonPressed; - bool rightMouseButtonPressed; - bool isDragging; - bool dragLeftViewport; - QModelIndex hovered; - QString hoveredCategory; - QPoint initialPressPosition; - QPoint mousePosition; - int forcedSelectionPosition; - - // Cache data - // We cannot merge some of them into structs because it would affect - // performance - QHash elementsInfo; - QHash elementsPosition; - QHash categoriesIndexes; - QHash categoriesPosition; - QStringList categories; - QModelIndexList intersectedIndexes; - QRect lastDraggedItemsRect; - int modelSortRole; - int modelSortColumn; - int modelLastRowCount; - bool modelCategorized; - Qt::SortOrder modelSortOrder; - QItemSelection lastSelection; - - // Attributes for speed reasons - KCategorizedSortFilterProxyModel *proxyModel; - QModelIndexList modelIndexList; + QHash blocks; }; #endif // KCATEGORIZEDVIEW_P_H diff --git a/kdeui/itemviews/kcategorydrawer.cpp b/kdeui/itemviews/kcategorydrawer.cpp index c0c10ac..3dbc9e4 100644 --- a/kdeui/itemviews/kcategorydrawer.cpp +++ b/kdeui/itemviews/kcategorydrawer.cpp @@ -29,12 +29,31 @@ #define HORIZONTAL_HINT 3 +class KCategoryDrawer::Private +{ +public: + Private() + : leftMargin(0) + , rightMargin(0) + { + } + + ~Private() + { + } + + int leftMargin; + int rightMargin; +}; + KCategoryDrawer::KCategoryDrawer() + : d(new Private) { } KCategoryDrawer::~KCategoryDrawer() { + delete d; } void KCategoryDrawer::drawCategory(const QModelIndex &index, @@ -102,3 +121,51 @@ int KCategoryDrawer::categoryHeight(const QModelIndex &index, const QStyleOption return option.fontMetrics.height() + 4 /* 3 separator; 1 gradient */; } + +int KCategoryDrawer::leftMargin() const +{ + return d->leftMargin; +} + +void KCategoryDrawer::setLeftMargin(int leftMargin) +{ + d->leftMargin = leftMargin; +} + +int KCategoryDrawer::rightMargin() const +{ + return d->rightMargin; +} + +void KCategoryDrawer::setRightMargin(int rightMargin) +{ + d->rightMargin = rightMargin; +} + +KCategoryDrawerV2::KCategoryDrawerV2(QObject *parent) + : QObject(parent) + , KCategoryDrawer() +{ +} + +KCategoryDrawerV2::~KCategoryDrawerV2() +{ +} + +void KCategoryDrawerV2::mouseButtonPressed(const QModelIndex &index, QMouseEvent *event) +{ +} + +void KCategoryDrawerV2::mouseButtonReleased(const QModelIndex &index, QMouseEvent *event) +{ +} + +void KCategoryDrawerV2::mouseButtonMoved(const QModelIndex &index, QMouseEvent *event) +{ +} + +void KCategoryDrawerV2::mouseButtonDoubleClicked(const QModelIndex &index, QMouseEvent *event) +{ +} + +#include "kcategorydrawer.moc" diff --git a/kdeui/itemviews/kcategorydrawer.h b/kdeui/itemviews/kcategorydrawer.h index 4e8894d..d4b9941 100644 --- a/kdeui/itemviews/kcategorydrawer.h +++ b/kdeui/itemviews/kcategorydrawer.h @@ -23,10 +23,16 @@ #include +#include +#include + class QPainter; class QModelIndex; class QStyleOption; +/** + * @warning Please use KCategoryDrawerV2 instead. + */ class KDEUI_EXPORT KCategoryDrawer { public: @@ -47,6 +53,58 @@ public: QPainter *painter) const; virtual int categoryHeight(const QModelIndex &index, const QStyleOption &option) const; + + //TODO KDE5: make virtual as leftMargin + /** + * @note 0 by default + * @since 4.3 + */ + int leftMargin() const; + /** + * @note call to this method on the KCategoryDrawer constructor to set the left margin + * @since 4.3 + */ + void setLeftMargin(int leftMargin); + + //TODO KDE5: make virtual as rightMargin + /** + * @note 0 by default + * @since 4.3 + */ + int rightMargin() const; + /** + * @note call to this method on the KCategoryDrawer constructor to set the right margin + * @since 4.3 + */ + void setRightMargin(int rightMargin); + +private: + class Private; + Private *const d; +}; + + +/** + * @since 4.3 + */ +class KDEUI_EXPORT KCategoryDrawerV2 + : public QObject + , public KCategoryDrawer +{ + Q_OBJECT + +public: + KCategoryDrawerV2(QObject *parent = 0); + + virtual ~KCategoryDrawerV2(); + + virtual void mouseButtonPressed(const QModelIndex &index, QMouseEvent *event); + virtual void mouseButtonReleased(const QModelIndex &index, QMouseEvent *event); + virtual void mouseButtonMoved(const QModelIndex &index, QMouseEvent *event); + virtual void mouseButtonDoubleClicked(const QModelIndex &index, QMouseEvent *event); + +Q_SIGNALS: + void collapseOrExpandClicked(const QModelIndex &index); }; #endif // KCATEGORYDRAWER_H