mirror of
https://bitbucket.org/smil3y/kdelibs.git
synced 2025-02-23 18:32:49 +00:00
1475 lines
56 KiB
C++
1475 lines
56 KiB
C++
/**
|
|
* This file is part of the KDE project
|
|
* Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public License
|
|
* along with this library; see the file COPYING.LIB. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
/**
|
|
* IMPLEMENTATION NOTES:
|
|
*
|
|
* QListView::setRowHidden() and QListView::isRowHidden() are not taken into
|
|
* account. This methods should actually not exist. This effect should be handled
|
|
* by an hypothetical QSortFilterProxyModel which filters out the desired rows.
|
|
*
|
|
* In case this needs to be implemented, contact me, but I consider this a faulty
|
|
* design.
|
|
*/
|
|
|
|
#include "kcategorizedview.h"
|
|
#include "kcategorizedview_p.h"
|
|
|
|
#include <QPainter>
|
|
#include <QScrollBar>
|
|
#include <QtGui/qevent.h>
|
|
|
|
#include "kcategorydrawer.h"
|
|
#include "kcategorizedsortfilterproxymodel.h"
|
|
|
|
//BEGIN: Private part
|
|
|
|
struct KCategorizedView::Private::Item
|
|
{
|
|
Item()
|
|
: topLeft(QPoint())
|
|
, size(QSize())
|
|
{
|
|
}
|
|
|
|
QPoint topLeft;
|
|
QSize size;
|
|
};
|
|
|
|
struct KCategorizedView::Private::Block
|
|
{
|
|
Block()
|
|
: topLeft(QPoint())
|
|
, height(-1)
|
|
, firstIndex(QModelIndex())
|
|
, quarantineStart(QModelIndex())
|
|
, items(QList<Item>())
|
|
, outOfQuarantine(false)
|
|
, alternate(false)
|
|
, collapsed(false)
|
|
{
|
|
}
|
|
|
|
bool operator!=(const Block &rhs) const
|
|
{
|
|
return firstIndex != rhs.firstIndex;
|
|
}
|
|
|
|
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<Item> 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;
|
|
};
|
|
|
|
KCategorizedView::Private::Private(KCategorizedView *q)
|
|
: q(q)
|
|
, proxyModel(0)
|
|
, categoryDrawer(0)
|
|
, categorySpacing(5)
|
|
, alternatingBlockColors(false)
|
|
, collapsibleBlocks(false)
|
|
, hoveredBlock(new Block())
|
|
, hoveredIndex(QModelIndex())
|
|
, pressedPosition(QPoint())
|
|
, rubberBandRect(QRect())
|
|
{
|
|
}
|
|
|
|
KCategorizedView::Private::~Private()
|
|
{
|
|
delete hoveredBlock;
|
|
}
|
|
|
|
bool KCategorizedView::Private::isCategorized() const
|
|
{
|
|
return proxyModel && categoryDrawer && proxyModel->isCategorizedModel();
|
|
}
|
|
|
|
QStyleOptionViewItem KCategorizedView::Private::blockRect(const QModelIndex &representative)
|
|
{
|
|
QStyleOptionViewItem option(q->viewOptions());
|
|
const int height = categoryDrawer->categoryHeight(representative, option);
|
|
const QString categoryDisplay = representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
|
|
QPoint pos = blockPosition(categoryDisplay);
|
|
pos.ry() -= height;
|
|
option.rect.setTopLeft(pos);
|
|
option.rect.setWidth(viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin());
|
|
option.rect.setHeight(height + blockHeight(categoryDisplay));
|
|
option.rect = mapToViewport(option.rect);
|
|
|
|
return option;
|
|
}
|
|
|
|
QPair<QModelIndex, QModelIndex> KCategorizedView::Private::intersectingIndexesWithRect(const QRect &_rect) const
|
|
{
|
|
const int rowCount = proxyModel->rowCount();
|
|
|
|
const QRect rect = _rect.normalized();
|
|
|
|
// binary search to find out the top border
|
|
int bottom = 0;
|
|
int top = rowCount - 1;
|
|
while (bottom <= top) {
|
|
const int middle = (bottom + top) / 2;
|
|
const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex());
|
|
const QRect itemRect = q->visualRect(index);
|
|
if (itemRect.bottomRight().y() <= rect.topLeft().y()) {
|
|
bottom = middle + 1;
|
|
} else {
|
|
top = middle - 1;
|
|
}
|
|
}
|
|
|
|
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());
|
|
const QRect itemRect = q->visualRect(index);
|
|
if (itemRect.topLeft().y() <= rect.bottomRight().y()) {
|
|
bottom = middle + 1;
|
|
} else {
|
|
top = middle - 1;
|
|
}
|
|
}
|
|
|
|
const QModelIndex topIndex = proxyModel->index(top, q->modelColumn(), q->rootIndex());
|
|
|
|
return qMakePair(bottomIndex, topIndex);
|
|
}
|
|
|
|
QPoint KCategorizedView::Private::blockPosition(const QString &category)
|
|
{
|
|
Block &block = blocks[category];
|
|
|
|
if (block.outOfQuarantine && !block.topLeft.isNull()) {
|
|
return block.topLeft;
|
|
}
|
|
|
|
QPoint res(categorySpacing, 0);
|
|
|
|
const QModelIndex index = block.firstIndex;
|
|
|
|
for (QHash<QString, Private::Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
|
|
Block &block = *it;
|
|
const QModelIndex categoryIndex = block.firstIndex;
|
|
if (index.row() < categoryIndex.row()) {
|
|
continue;
|
|
}
|
|
res.ry() += categoryDrawer->categoryHeight(categoryIndex, q->viewOptions()) + categorySpacing;
|
|
if (index.row() == categoryIndex.row()) {
|
|
continue;
|
|
}
|
|
res.ry() += blockHeight(it.key());
|
|
}
|
|
|
|
block.outOfQuarantine = true;
|
|
block.topLeft = res;
|
|
|
|
return res;
|
|
}
|
|
|
|
int KCategorizedView::Private::blockHeight(const QString &category)
|
|
{
|
|
Block &block = blocks[category];
|
|
|
|
if (block.collapsed) {
|
|
return 0;
|
|
}
|
|
|
|
if (block.height > -1) {
|
|
return block.height;
|
|
}
|
|
|
|
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);
|
|
|
|
if (hasGrid()) {
|
|
bottomRight.setHeight(qMax(bottomRight.height(), q->gridSize().height()));
|
|
} else {
|
|
if (!q->uniformItemSizes()) {
|
|
bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2);
|
|
}
|
|
}
|
|
|
|
const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1;
|
|
block.height = height;
|
|
|
|
return height;
|
|
}
|
|
|
|
int KCategorizedView::Private::viewportWidth() const
|
|
{
|
|
return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin();
|
|
}
|
|
|
|
void KCategorizedView::Private::regenerateAllElements()
|
|
{
|
|
for (QHash<QString, Block>::Iterator it = blocks.begin(); it != blocks.end(); ++it) {
|
|
Block &block = *it;
|
|
block.outOfQuarantine = false;
|
|
block.quarantineStart = block.firstIndex;
|
|
block.height = -1;
|
|
}
|
|
}
|
|
|
|
void KCategorizedView::Private::rowsInserted(const QModelIndex &parent, int start, int end)
|
|
{
|
|
if (!isCategorized()) {
|
|
return;
|
|
}
|
|
|
|
for (int i = start; i <= end; ++i) {
|
|
const QModelIndex index = proxyModel->index(i, q->modelColumn(), parent);
|
|
|
|
Q_ASSERT(index.isValid());
|
|
|
|
const QString category = categoryForIndex(index);
|
|
|
|
Block &block = blocks[category];
|
|
|
|
//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
|
|
|
|
Q_ASSERT(block.firstIndex.isValid());
|
|
|
|
const int firstIndexRow = block.firstIndex.row();
|
|
|
|
block.items.insert(index.row() - firstIndexRow, Private::Item());
|
|
block.height = -1;
|
|
|
|
q->visualRect(index);
|
|
q->viewport()->update();
|
|
}
|
|
|
|
//BEGIN: update the items that are in quarantine in affected categories
|
|
{
|
|
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
|
|
|
|
//BEGIN: mark as in quarantine those categories that are under the affected ones
|
|
{
|
|
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<Block> blockList = blocks.values();
|
|
qSort(blockList.begin(), blockList.end(), Block::lessThan);
|
|
QList<int> firstIndexesRows;
|
|
foreach (const Block &block, blockList) {
|
|
firstIndexesRows << block.firstIndex.row();
|
|
}
|
|
//END: order for marking as alternate those blocks that are alternate
|
|
for (QHash<QString, Private::Block>::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;
|
|
}
|
|
}
|
|
}
|
|
//END: mark as in quarantine those categories that are under the affected ones
|
|
}
|
|
|
|
QRect KCategorizedView::Private::mapToViewport(const QRect &rect) const
|
|
{
|
|
const int dx = -q->horizontalOffset();
|
|
const int dy = -q->verticalOffset();
|
|
return rect.adjusted(dx, dy, dx, dy);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
int KCategorizedView::Private::highestElementInLastRow(const Block &block) const
|
|
{
|
|
//Find the highest element in the last row
|
|
const QModelIndex lastIndex = proxyModel->index(block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex());
|
|
const QRect prevRect = q->visualRect(lastIndex);
|
|
int res = prevRect.height();
|
|
QModelIndex prevIndex = proxyModel->index(lastIndex.row() - 1, q->modelColumn(), q->rootIndex());
|
|
if (!prevIndex.isValid()) {
|
|
return res;
|
|
}
|
|
Q_FOREVER {
|
|
const QRect tempRect = q->visualRect(prevIndex);
|
|
if (tempRect.topLeft().y() < prevRect.topLeft().y()) {
|
|
break;
|
|
}
|
|
res = qMax(res, tempRect.height());
|
|
if (prevIndex == block.firstIndex) {
|
|
break;
|
|
}
|
|
prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex());
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
bool KCategorizedView::Private::hasGrid() const
|
|
{
|
|
const QSize gridSize = q->gridSize();
|
|
return gridSize.isValid() && !gridSize.isNull();
|
|
}
|
|
|
|
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::leftToRightVisualRect(const QModelIndex &index, Item &item,
|
|
const Block &block, const QPoint &blockPos) const
|
|
{
|
|
const int firstIndexRow = block.firstIndex.row();
|
|
|
|
if (hasGrid()) {
|
|
const int relativeRow = index.row() - firstIndexRow;
|
|
const int maxItemsPerRow = qMax(viewportWidth() / q->gridSize().width(), 1);
|
|
if (q->layoutDirection() == Qt::LeftToRight) {
|
|
item.topLeft.rx() = (relativeRow % maxItemsPerRow) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin();
|
|
} else {
|
|
item.topLeft.rx() = viewportWidth() - ((relativeRow % maxItemsPerRow) + 1) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing;
|
|
}
|
|
item.topLeft.ry() = (relativeRow / maxItemsPerRow) * q->gridSize().height();
|
|
} else {
|
|
if (q->uniformItemSizes()) {
|
|
const int relativeRow = index.row() - firstIndexRow;
|
|
const QSize itemSize = q->sizeHintForIndex(index);
|
|
const int maxItemsPerRow = qMax((viewportWidth() - q->spacing()) / (itemSize.width() + q->spacing()), 1);
|
|
if (q->layoutDirection() == Qt::LeftToRight) {
|
|
item.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin();
|
|
} else {
|
|
item.topLeft.rx() = viewportWidth() - (relativeRow % maxItemsPerRow) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing;
|
|
}
|
|
item.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height();
|
|
} else {
|
|
const QSize currSize = q->sizeHintForIndex(index);
|
|
if (index != block.firstIndex) {
|
|
const int viewportW = viewportWidth() - q->spacing();
|
|
QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
|
|
QRect prevRect = q->visualRect(prevIndex);
|
|
prevRect = mapFromViewport(prevRect);
|
|
if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + q->spacing() > viewportW) {
|
|
// we have to check the whole previous row, and see which one was the
|
|
// highest.
|
|
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;
|
|
}
|
|
if (tempRect.bottomRight().y() > prevRect.bottomRight().y()) {
|
|
prevRect = tempRect;
|
|
}
|
|
if (prevIndex == block.firstIndex) {
|
|
break;
|
|
}
|
|
}
|
|
if (q->layoutDirection() == Qt::LeftToRight) {
|
|
item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing();
|
|
} else {
|
|
item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
|
|
}
|
|
item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
|
|
} else {
|
|
if (q->layoutDirection() == Qt::LeftToRight) {
|
|
item.topLeft.rx() = (prevRect.bottomRight().x() + 1) + q->spacing();
|
|
} else {
|
|
item.topLeft.rx() = (prevRect.bottomLeft().x() - 1) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing;
|
|
}
|
|
item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y();
|
|
}
|
|
} else {
|
|
if (q->layoutDirection() == Qt::LeftToRight) {
|
|
item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
|
|
} else {
|
|
item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing;
|
|
}
|
|
item.topLeft.ry() = q->spacing();
|
|
}
|
|
}
|
|
}
|
|
item.size = q->sizeHintForIndex(index);
|
|
}
|
|
|
|
void KCategorizedView::Private::topToBottomVisualRect(const QModelIndex &index, Item &item,
|
|
const Block &block, const QPoint &blockPos) const
|
|
{
|
|
const int firstIndexRow = block.firstIndex.row();
|
|
|
|
if (hasGrid()) {
|
|
const int relativeRow = index.row() - firstIndexRow;
|
|
item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
|
|
item.topLeft.ry() = relativeRow * q->gridSize().height();
|
|
} else {
|
|
if (q->uniformItemSizes()) {
|
|
const int relativeRow = index.row() - firstIndexRow;
|
|
const QSize itemSize = q->sizeHintForIndex(index);
|
|
item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin();
|
|
item.topLeft.ry() = relativeRow * itemSize.height();
|
|
} else {
|
|
if (index != block.firstIndex) {
|
|
QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex());
|
|
QRect prevRect = q->visualRect(prevIndex);
|
|
prevRect = mapFromViewport(prevRect);
|
|
item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
|
|
item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y();
|
|
} else {
|
|
item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing();
|
|
item.topLeft.ry() = q->spacing();
|
|
}
|
|
}
|
|
}
|
|
item.size = q->sizeHintForIndex(index);
|
|
item.size.setWidth(viewportWidth());
|
|
}
|
|
|
|
void KCategorizedView::Private::_k_slotCollapseOrExpandClicked(QModelIndex)
|
|
{
|
|
}
|
|
|
|
//END: Private part
|
|
|
|
//BEGIN: Public part
|
|
|
|
KCategorizedView::KCategorizedView(QWidget *parent)
|
|
: QListView(parent)
|
|
, d(new Private(this))
|
|
{
|
|
}
|
|
|
|
KCategorizedView::~KCategorizedView()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
void KCategorizedView::setModel(QAbstractItemModel *model)
|
|
{
|
|
if (d->proxyModel == model) {
|
|
return;
|
|
}
|
|
|
|
d->blocks.clear();
|
|
|
|
if (d->proxyModel) {
|
|
disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
|
|
}
|
|
|
|
d->proxyModel = qobject_cast<KCategorizedSortFilterProxyModel*>(model);
|
|
|
|
if (d->proxyModel) {
|
|
connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged()));
|
|
}
|
|
|
|
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)
|
|
{
|
|
setGridSizeOwn(size);
|
|
}
|
|
|
|
void KCategorizedView::setGridSizeOwn(const QSize &size)
|
|
{
|
|
d->regenerateAllElements();
|
|
QListView::setGridSize(size);
|
|
}
|
|
|
|
QRect KCategorizedView::visualRect(const QModelIndex &index) const
|
|
{
|
|
if (!d->isCategorized()) {
|
|
return QListView::visualRect(index);
|
|
}
|
|
|
|
if (!index.isValid()) {
|
|
return QRect();
|
|
}
|
|
|
|
const QString category = d->categoryForIndex(index);
|
|
|
|
if (!d->blocks.contains(category)) {
|
|
return QRect();
|
|
}
|
|
|
|
Private::Block &block = d->blocks[category];
|
|
const int firstIndexRow = block.firstIndex.row();
|
|
|
|
Q_ASSERT(block.firstIndex.isValid());
|
|
|
|
if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) {
|
|
return QRect();
|
|
}
|
|
|
|
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 (flow() == LeftToRight) {
|
|
d->leftToRightVisualRect(index, ritem, block, blockPos);
|
|
} else {
|
|
d->topToBottomVisualRect(index, ritem, block, blockPos);
|
|
}
|
|
|
|
//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());
|
|
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);
|
|
}
|
|
|
|
QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height());
|
|
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);
|
|
}
|
|
|
|
KCategoryDrawer *KCategorizedView::categoryDrawer() const
|
|
{
|
|
return d->categoryDrawer;
|
|
}
|
|
|
|
void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer)
|
|
{
|
|
if (d->categoryDrawer) {
|
|
disconnect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)),
|
|
this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
|
|
}
|
|
|
|
d->categoryDrawer = categoryDrawer;
|
|
|
|
if (d->categoryDrawer) {
|
|
connect(d->categoryDrawer, SIGNAL(collapseOrExpandClicked(QModelIndex)),
|
|
this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex)));
|
|
}
|
|
}
|
|
|
|
int KCategorizedView::categorySpacing() const
|
|
{
|
|
return d->categorySpacing;
|
|
}
|
|
|
|
void KCategorizedView::setCategorySpacing(int categorySpacing)
|
|
{
|
|
if (d->categorySpacing == categorySpacing) {
|
|
return;
|
|
}
|
|
|
|
d->categorySpacing = categorySpacing;
|
|
|
|
for (QHash<QString, Private::Block>::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) {
|
|
Private::Block &block = *it;
|
|
block.outOfQuarantine = false;
|
|
}
|
|
}
|
|
|
|
bool KCategorizedView::alternatingBlockColors() const
|
|
{
|
|
return d->alternatingBlockColors;
|
|
}
|
|
|
|
void KCategorizedView::setAlternatingBlockColors(bool enable)
|
|
{
|
|
d->alternatingBlockColors = enable;
|
|
}
|
|
|
|
bool KCategorizedView::collapsibleBlocks() const
|
|
{
|
|
return d->collapsibleBlocks;
|
|
}
|
|
|
|
void KCategorizedView::setCollapsibleBlocks(bool enable)
|
|
{
|
|
d->collapsibleBlocks = enable;
|
|
}
|
|
|
|
QModelIndexList KCategorizedView::block(const QString &category)
|
|
{
|
|
QModelIndexList res;
|
|
const Private::Block &block = d->blocks[category];
|
|
if (block.height == -1) {
|
|
return res;
|
|
}
|
|
QModelIndex current = block.firstIndex;
|
|
const int first = current.row();
|
|
for (int i = 1; i <= block.items.count(); ++i) {
|
|
if (current.isValid()) {
|
|
res << current;
|
|
}
|
|
current = d->proxyModel->index(first + i, modelColumn(), rootIndex());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
QModelIndexList KCategorizedView::block(const QModelIndex &representative)
|
|
{
|
|
return block(representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString());
|
|
}
|
|
|
|
QModelIndex KCategorizedView::indexAt(const QPoint &point) const
|
|
{
|
|
if (!d->isCategorized()) {
|
|
return QListView::indexAt(point);
|
|
}
|
|
|
|
const int rowCount = d->proxyModel->rowCount();
|
|
if (!rowCount) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
// 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());
|
|
const QRect rect = visualRect(index);
|
|
if (rect.contains(point)) {
|
|
if (index.model()->flags(index) & Qt::ItemIsEnabled) {
|
|
return index;
|
|
}
|
|
return QModelIndex();
|
|
}
|
|
bool directionCondition;
|
|
if (layoutDirection() == Qt::LeftToRight) {
|
|
directionCondition = point.x() >= rect.bottomLeft().x();
|
|
} else {
|
|
directionCondition = point.x() <= rect.bottomRight().x();
|
|
}
|
|
if (point.y() < rect.topLeft().y()) {
|
|
top = middle - 1;
|
|
} else if (directionCondition) {
|
|
bottom = middle + 1;
|
|
} else if (point.y() <= rect.bottomRight().y()) {
|
|
top = middle - 1;
|
|
} else {
|
|
bool after = true;
|
|
for (int i = middle - 1;i >= bottom;i--) {
|
|
const QModelIndex newIndex =
|
|
d->proxyModel->index(i, modelColumn(), rootIndex());
|
|
const QRect newRect = visualRect(newIndex);
|
|
if (newRect.topLeft().y() < rect.topLeft().y()) {
|
|
break;
|
|
} else if (newRect.contains(point)) {
|
|
if (newIndex.model()->flags(newIndex) & Qt::ItemIsEnabled) {
|
|
return newIndex;
|
|
}
|
|
return QModelIndex();
|
|
} else if ((layoutDirection() == Qt::LeftToRight) ?
|
|
(newRect.topLeft().x() <= point.x()) :
|
|
(newRect.topRight().x() >= point.x())) {
|
|
break;
|
|
} else if (newRect.bottomRight().y() >= point.y()) {
|
|
after = false;
|
|
}
|
|
}
|
|
if (!after) {
|
|
return QModelIndex();
|
|
}
|
|
bottom = middle + 1;
|
|
}
|
|
}
|
|
return QModelIndex();
|
|
}
|
|
|
|
void KCategorizedView::reset()
|
|
{
|
|
d->blocks.clear();
|
|
QListView::reset();
|
|
}
|
|
|
|
void KCategorizedView::paintEvent(QPaintEvent *event)
|
|
{
|
|
if (!d->isCategorized()) {
|
|
QListView::paintEvent(event);
|
|
return;
|
|
}
|
|
|
|
const QPair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(viewport()->rect().intersected(event->rect()));
|
|
|
|
QPainter p(viewport());
|
|
p.save();
|
|
|
|
Q_ASSERT(selectionModel()->model() == d->proxyModel);
|
|
|
|
//BEGIN: draw categories
|
|
QHash<QString, Private::Block>::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());
|
|
QStyleOptionViewItem option(viewOptions());
|
|
option.features |= d->alternatingBlockColors && block.alternate ? QStyleOptionViewItem::Alternate
|
|
: QStyleOptionViewItem::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;
|
|
QModelIndex categoryIndex;
|
|
QString category;
|
|
Private::Block *block = 0;
|
|
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) {
|
|
categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
|
|
category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
|
|
block = &d->blocks[category];
|
|
indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count();
|
|
if (block->collapsed) {
|
|
i = indexToCheckIfBlockCollapsed;
|
|
continue;
|
|
}
|
|
}
|
|
//END: first check if the block is collapsed. if so, we have to skip the item painting
|
|
|
|
Q_ASSERT(block);
|
|
|
|
const bool alternateItem = (i - block->firstIndex.row()) % 2;
|
|
|
|
const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex());
|
|
const Qt::ItemFlags flags = d->proxyModel->flags(index);
|
|
QStyleOptionViewItem option(viewOptions());
|
|
option.rect = visualRect(index);
|
|
option.widget = this;
|
|
option.features |= wordWrap() ? QStyleOptionViewItem::WrapText
|
|
: QStyleOptionViewItem::None;
|
|
option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItem::Alternate
|
|
: QStyleOptionViewItem::None;
|
|
if (flags & Qt::ItemIsSelectable) {
|
|
option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
|
|
: QStyle::State_None;
|
|
} else {
|
|
option.state &= ~QStyle::State_Selected;
|
|
}
|
|
option.state |= (index == currentIndex()) ? QStyle::State_HasFocus
|
|
: QStyle::State_None;
|
|
if (!(flags & Qt::ItemIsEnabled)) {
|
|
option.state &= ~QStyle::State_Enabled;
|
|
} else {
|
|
option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver
|
|
: QStyle::State_None;
|
|
}
|
|
|
|
itemDelegate(index)->paint(&p, option, index);
|
|
++i;
|
|
}
|
|
//END: draw items
|
|
}
|
|
|
|
//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
|
|
|
|
p.restore();
|
|
}
|
|
|
|
void KCategorizedView::resizeEvent(QResizeEvent *event)
|
|
{
|
|
d->regenerateAllElements();
|
|
QListView::resizeEvent(event);
|
|
}
|
|
|
|
void KCategorizedView::setSelection(const QRect &rect,
|
|
QItemSelectionModel::SelectionFlags flags)
|
|
{
|
|
if (!d->isCategorized()) {
|
|
QListView::setSelection(rect, flags);
|
|
return;
|
|
}
|
|
|
|
if (rect.topLeft() == rect.bottomRight()) {
|
|
const QModelIndex index = indexAt(rect.topLeft());
|
|
selectionModel()->select(index, flags);
|
|
return;
|
|
}
|
|
|
|
const QPair<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect(rect);
|
|
|
|
QItemSelection selection;
|
|
|
|
//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();
|
|
}
|
|
} else if (visualRectIntersects) {
|
|
firstIndex = index;
|
|
lastIndex = index;
|
|
}
|
|
}
|
|
|
|
if (firstIndex.isValid()) {
|
|
selection << QItemSelectionRange(firstIndex, lastIndex);
|
|
}
|
|
|
|
selectionModel()->select(selection, flags);
|
|
}
|
|
|
|
void KCategorizedView::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
QListView::mouseMoveEvent(event);
|
|
d->hoveredIndex = indexAt(event->pos());
|
|
const SelectionMode itemViewSelectionMode = selectionMode();
|
|
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;
|
|
}
|
|
if (!d->categoryDrawer) {
|
|
return;
|
|
}
|
|
QHash<QString, Private::Block>::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());
|
|
QStyleOptionViewItem option(viewOptions());
|
|
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);
|
|
const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
|
|
if (option.rect.contains(mousePos)) {
|
|
if (d->categoryDrawer && d->hoveredBlock->height != -1 && *d->hoveredBlock != block) {
|
|
const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
|
|
const QStyleOptionViewItem option = d->blockRect(categoryIndex);
|
|
d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
|
|
*d->hoveredBlock = block;
|
|
d->hoveredCategory = it.key();
|
|
viewport()->update(option.rect);
|
|
} else if (d->hoveredBlock->height == -1) {
|
|
*d->hoveredBlock = block;
|
|
d->hoveredCategory = it.key();
|
|
} else {
|
|
d->categoryDrawer->mouseMoved(categoryIndex, option.rect, event);
|
|
}
|
|
viewport()->update(option.rect);
|
|
return;
|
|
}
|
|
++it;
|
|
}
|
|
if (d->categoryDrawer && d->hoveredBlock->height != -1) {
|
|
const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
|
|
const QStyleOptionViewItem option = d->blockRect(categoryIndex);
|
|
d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
viewport()->update(option.rect);
|
|
}
|
|
}
|
|
|
|
void KCategorizedView::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
if (event->button() == Qt::LeftButton) {
|
|
d->pressedPosition = event->pos();
|
|
d->pressedPosition.rx() += horizontalOffset();
|
|
d->pressedPosition.ry() += verticalOffset();
|
|
}
|
|
if (!d->categoryDrawer) {
|
|
QListView::mousePressEvent(event);
|
|
return;
|
|
}
|
|
QHash<QString, Private::Block>::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());
|
|
const QStyleOptionViewItem option = d->blockRect(categoryIndex);
|
|
const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
|
|
if (option.rect.contains(mousePos)) {
|
|
d->categoryDrawer->mouseButtonPressed(categoryIndex, option.rect, event);
|
|
viewport()->update(option.rect);
|
|
if (!event->isAccepted()) {
|
|
QListView::mousePressEvent(event);
|
|
}
|
|
return;
|
|
}
|
|
++it;
|
|
}
|
|
QListView::mousePressEvent(event);
|
|
}
|
|
|
|
void KCategorizedView::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
d->pressedPosition = QPoint();
|
|
d->rubberBandRect = QRect();
|
|
if (!d->categoryDrawer) {
|
|
QListView::mouseReleaseEvent(event);
|
|
return;
|
|
}
|
|
QHash<QString, Private::Block>::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());
|
|
const QStyleOptionViewItem option = d->blockRect(categoryIndex);
|
|
const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos());
|
|
if (option.rect.contains(mousePos)) {
|
|
d->categoryDrawer->mouseButtonReleased(categoryIndex, option.rect, event);
|
|
viewport()->update(option.rect);
|
|
if (!event->isAccepted()) {
|
|
QListView::mouseReleaseEvent(event);
|
|
}
|
|
return;
|
|
}
|
|
++it;
|
|
}
|
|
QListView::mouseReleaseEvent(event);
|
|
}
|
|
|
|
void KCategorizedView::leaveEvent(QEvent *event)
|
|
{
|
|
QListView::leaveEvent(event);
|
|
if (d->hoveredIndex.isValid()) {
|
|
viewport()->update(visualRect(d->hoveredIndex));
|
|
d->hoveredIndex = QModelIndex();
|
|
}
|
|
if (d->categoryDrawer && d->hoveredBlock->height != -1) {
|
|
const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex());
|
|
const QStyleOptionViewItem option = d->blockRect(categoryIndex);
|
|
d->categoryDrawer->mouseLeft(categoryIndex, option.rect);
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
viewport()->update(option.rect);
|
|
}
|
|
}
|
|
|
|
void KCategorizedView::dragMoveEvent(QDragMoveEvent *event)
|
|
{
|
|
QListView::dragMoveEvent(event);
|
|
d->hoveredIndex = indexAt(event->pos());
|
|
}
|
|
|
|
//TODO: improve se we take into account collapsed blocks
|
|
//TODO: take into account when there is no grid and no uniformItemSizes
|
|
QModelIndex KCategorizedView::moveCursor(CursorAction cursorAction,
|
|
Qt::KeyboardModifiers modifiers)
|
|
{
|
|
if (!d->isCategorized()) {
|
|
return QListView::moveCursor(cursorAction, modifiers);
|
|
}
|
|
|
|
const QModelIndex current = currentIndex();
|
|
const QRect currentRect = visualRect(current);
|
|
if (!current.isValid()) {
|
|
const int rowCount = d->proxyModel->rowCount(rootIndex());
|
|
if (!rowCount) {
|
|
return QModelIndex();
|
|
}
|
|
return d->proxyModel->index(0, modelColumn(), rootIndex());
|
|
}
|
|
|
|
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() || uniformItemSizes()) {
|
|
const QModelIndex current = currentIndex();
|
|
const QSize itemSize = d->hasGrid() ? gridSize()
|
|
: sizeHintForIndex(current);
|
|
const Private::Block &block = d->blocks[d->categoryForIndex(current)];
|
|
const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
|
|
const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() +
|
|
block.items.count();
|
|
|
|
if (canMove) {
|
|
return d->proxyModel->index(current.row() + maxItemsPerRow, modelColumn(), rootIndex());
|
|
}
|
|
|
|
const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
|
|
const QModelIndex nextIndex = d->proxyModel->index(block.firstIndex.row() + block.items.count(), modelColumn(), rootIndex());
|
|
|
|
if (!nextIndex.isValid()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
const Private::Block &nextBlock = d->blocks[d->categoryForIndex(nextIndex)];
|
|
|
|
if (nextBlock.items.count() <= currentRelativePos) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
if (currentRelativePos < (block.items.count() % maxItemsPerRow)) {
|
|
return d->proxyModel->index(nextBlock.firstIndex.row() + currentRelativePos, modelColumn(), rootIndex());
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
}
|
|
case MoveUp: {
|
|
if (d->hasGrid() || uniformItemSizes()) {
|
|
const QModelIndex current = currentIndex();
|
|
const QSize itemSize = d->hasGrid() ? gridSize()
|
|
: sizeHintForIndex(current);
|
|
const Private::Block &block = d->blocks[d->categoryForIndex(current)];
|
|
const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1);
|
|
const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row();
|
|
|
|
if (canMove) {
|
|
return d->proxyModel->index(current.row() - maxItemsPerRow, modelColumn(), rootIndex());
|
|
}
|
|
|
|
const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow;
|
|
const QModelIndex prevIndex = d->proxyModel->index(block.firstIndex.row() - 1, modelColumn(), rootIndex());
|
|
|
|
if (!prevIndex.isValid()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
const Private::Block &prevBlock = d->blocks[d->categoryForIndex(prevIndex)];
|
|
|
|
if (prevBlock.items.count() <= currentRelativePos) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
const int remainder = prevBlock.items.count() % maxItemsPerRow;
|
|
if (currentRelativePos < remainder) {
|
|
return d->proxyModel->index(prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, modelColumn(), rootIndex());
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
void KCategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent,
|
|
int start,
|
|
int end)
|
|
{
|
|
if (!d->isCategorized()) {
|
|
QListView::rowsAboutToBeRemoved(parent, start, end);
|
|
return;
|
|
}
|
|
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
|
|
if (end - start + 1 == d->proxyModel->rowCount()) {
|
|
d->blocks.clear();
|
|
QListView::rowsAboutToBeRemoved(parent, start, end);
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
Private::Block &block = d->blocks[category];
|
|
block.items.removeAt(i - block.firstIndex.row() - alreadyRemoved);
|
|
++alreadyRemoved;
|
|
|
|
if (!block.items.count()) {
|
|
listOfCategoriesMarkedForRemoval << category;
|
|
}
|
|
|
|
block.height = -1;
|
|
|
|
viewport()->update();
|
|
}
|
|
|
|
//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
|
|
|
|
Q_FOREACH (const QString &category, listOfCategoriesMarkedForRemoval) {
|
|
d->blocks.remove(category);
|
|
}
|
|
|
|
//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<Private::Block> blockList = d->blocks.values();
|
|
qSort(blockList.begin(), blockList.end(), Private::Block::lessThan);
|
|
QList<int> firstIndexesRows;
|
|
foreach (const Private::Block &block, blockList) {
|
|
firstIndexesRows << block.firstIndex.row();
|
|
}
|
|
//END: order for marking as alternate those blocks that are alternate
|
|
for (QHash<QString, Private::Block>::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
|
|
|
|
QListView::rowsAboutToBeRemoved(parent, start, end);
|
|
}
|
|
|
|
void KCategorizedView::updateGeometries()
|
|
{
|
|
const int oldVerticalOffset = verticalOffset();
|
|
const Qt::ScrollBarPolicy verticalP = verticalScrollBarPolicy(), horizontalP = horizontalScrollBarPolicy();
|
|
|
|
//BEGIN bugs 213068, 287847 ------------------------------------------------------------
|
|
/*
|
|
* QListView::updateGeometries() has it's own opinion on whether the scrollbars should be visible (valid range) or not
|
|
* and triggers a (sometimes additionally timered) resize through ::layoutChildren()
|
|
* http://qt.gitorious.org/qt/qt/blobs/4.7/src/gui/itemviews/qlistview.cpp#line1499
|
|
* (the comment above the main block isn't all accurate, layoutChldren is called regardless of the policy)
|
|
*
|
|
* As a result QListView and KCategorizedView occasionally started a race on the scrollbar visibility, effectively blocking the UI
|
|
* So we prevent QListView from having an own opinion on the scrollbar visibility by
|
|
* fixing it before calling the baseclass QListView::updateGeometries()
|
|
*
|
|
* Since the implicit show/hide by the followin range setting will cause further resizes if the policy is Qt::ScrollBarAsNeeded
|
|
* we keep it static until we're done, then restore the original value and ultimately change the scrollbar visibility ourself.
|
|
*/
|
|
if (d->isCategorized()) { // important! - otherwise we'd pollute the setting if the view is initially not categorized
|
|
setVerticalScrollBarPolicy((verticalP == Qt::ScrollBarAlwaysOn || verticalScrollBar()->isVisibleTo(this)) ?
|
|
Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
|
setHorizontalScrollBarPolicy((horizontalP == Qt::ScrollBarAlwaysOn || horizontalScrollBar()->isVisibleTo(this)) ?
|
|
Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
|
}
|
|
//END bugs 213068, 287847 --------------------------------------------------------------
|
|
|
|
QListView::updateGeometries();
|
|
|
|
if (!d->isCategorized()) {
|
|
return;
|
|
}
|
|
|
|
const int rowCount = d->proxyModel->rowCount();
|
|
if (!rowCount) {
|
|
verticalScrollBar()->setRange(0, 0);
|
|
// unconditional, see function end todo
|
|
horizontalScrollBar()->setRange(0, 0);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height();
|
|
|
|
if (verticalScrollMode() == ScrollPerItem) {
|
|
verticalScrollBar()->setSingleStep(lastItemRect.height());
|
|
const int rowsPerPage = qMax(viewport()->height() / lastItemRect.height(), 1);
|
|
verticalScrollBar()->setPageStep(rowsPerPage * lastItemRect.height());
|
|
}
|
|
|
|
verticalScrollBar()->setRange(0, bottomRange);
|
|
verticalScrollBar()->setValue(oldVerticalOffset);
|
|
|
|
//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), 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);
|
|
|
|
//BEGIN bugs 213068, 287847 ------------------------------------------------------------
|
|
// restoring values from above ...
|
|
setVerticalScrollBarPolicy(verticalP);
|
|
setHorizontalScrollBarPolicy(horizontalP);
|
|
// ... and correct the visibility
|
|
bool validRange = verticalScrollBar()->maximum() != verticalScrollBar()->minimum();
|
|
if (verticalP == Qt::ScrollBarAsNeeded && (verticalScrollBar()->isVisibleTo(this) != validRange))
|
|
verticalScrollBar()->setVisible(validRange);
|
|
validRange = horizontalScrollBar()->maximum() > horizontalScrollBar()->minimum();
|
|
if (horizontalP == Qt::ScrollBarAsNeeded && (horizontalScrollBar()->isVisibleTo(this) != validRange))
|
|
horizontalScrollBar()->setVisible(validRange);
|
|
//END bugs 213068, 287847 --------------------------------------------------------------
|
|
}
|
|
|
|
void KCategorizedView::dataChanged(const QModelIndex &topLeft,
|
|
const QModelIndex &bottomRight)
|
|
{
|
|
QListView::dataChanged(topLeft, bottomRight);
|
|
if (!d->isCategorized()) {
|
|
return;
|
|
}
|
|
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
|
|
//BEGIN: since the model changed data, we need to reconsider item sizes
|
|
int i = topLeft.row();
|
|
int indexToCheck = i;
|
|
QModelIndex categoryIndex;
|
|
QString category;
|
|
Private::Block *block;
|
|
while (i <= bottomRight.row()) {
|
|
const QModelIndex currIndex = d->proxyModel->index(i, modelColumn(), rootIndex());
|
|
if (i == indexToCheck) {
|
|
categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex());
|
|
category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString();
|
|
block = &d->blocks[category];
|
|
block->quarantineStart = currIndex;
|
|
indexToCheck = block->firstIndex.row() + block->items.count();
|
|
}
|
|
visualRect(currIndex);
|
|
++i;
|
|
}
|
|
//END: since the model changed data, we need to reconsider item sizes
|
|
}
|
|
|
|
void KCategorizedView::rowsInserted(const QModelIndex &parent,
|
|
int start,
|
|
int end)
|
|
{
|
|
QListView::rowsInserted(parent, start, end);
|
|
if (!d->isCategorized()) {
|
|
return;
|
|
}
|
|
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
d->rowsInserted(parent, start, end);
|
|
}
|
|
|
|
|
|
|
|
void KCategorizedView::slotLayoutChanged()
|
|
{
|
|
if (!d->isCategorized()) {
|
|
return;
|
|
}
|
|
|
|
d->blocks.clear();
|
|
*d->hoveredBlock = Private::Block();
|
|
d->hoveredCategory = QString();
|
|
if (d->proxyModel->rowCount()) {
|
|
d->rowsInserted(rootIndex(), 0, d->proxyModel->rowCount() - 1);
|
|
}
|
|
}
|
|
|
|
//END: Public part
|
|
|
|
#include "moc_kcategorizedview.cpp"
|