/**
  * 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 <math.h> // trunc on C99 compliant systems
//#include <kdefakes.h> // trunc for not C99 compliant systems

#include <QPainter>
#include <QScrollBar>
#include <QPaintEvent>

#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() )
	, constantItemWidth( 0 )
{
}

KCategorizedView::Private::~Private()
{
	delete hoveredBlock;
}

bool KCategorizedView::Private::isCategorized() const
{
	return proxyModel && categoryDrawer && proxyModel->isCategorizedModel();
}

QStyleOptionViewItemV4 KCategorizedView::Private::blockRect ( const QModelIndex &representative )
{
	QStyleOptionViewItemV4 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() );
		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
		{
			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() );
		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;
		}
	}

	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() /*|| q->uniformItemWidths()*/ )
		{
			const int relativeRow = index.row() - firstIndexRow;
			const QSize itemSize = q->sizeHintForIndex ( index );
			//HACK: Why is the -2 needed?
			const int maxItemsPerRow = qMax ( ( viewportWidth() - q->spacing() - 2 ) / ( 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() );
						QRect tempRect = q->visualRect ( prevIndex );
						tempRect = mapFromViewport ( tempRect );
						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 = dynamic_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::setUniformItemWidths(bool enable)
{
	d->constantItemWidth = enable;
}


bool KCategorizedView::uniformItemWidths() const
{
	return d->constantItemWidth;
}

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 )
{
	disconnect ( d->categoryDrawer, SIGNAL ( collapseOrExpandClicked ( QModelIndex ) ),
	             this, SLOT ( slotCollapseOrExpandClicked ( QModelIndex ) ) );
	d->categoryDrawer = categoryDrawer;

	connect ( d->categoryDrawer, SIGNAL ( collapseOrExpandClicked ( QModelIndex ) ),
	          this, SLOT ( 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() );
		QRect rect = visualRect ( index );
		const int verticalOff = verticalOffset();
		int horizontalOff = horizontalOffset();
		if ( layoutDirection() == Qt::RightToLeft )
		{
			horizontalOff *= -1;
		}
		rect.topLeft().ry() += verticalOff;
		rect.topLeft().rx() += horizontalOff;
		rect.bottomRight().ry() += verticalOff;
		rect.bottomRight().rx() += horizontalOff;
		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.bottomRight().x();
		}
		else
		{
			directionCondition = point.x() < rect.bottomLeft().x();
		}
		if ( point.y() > rect.bottomRight().y() ||
		     ( point.y() > rect.topLeft().y() && point.y() < rect.bottomRight().y() && directionCondition ) )
		{
			bottom = middle + 1;
		}
		else
		{
			top = 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() );
		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;
		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 );
			QStyleOptionViewItemV4 option ( viewOptions() );
			option.rect = visualRect ( index );
			option.widget = this;
			option.features |= wordWrap() ? QStyleOptionViewItemV2::WrapText
			                   : QStyleOptionViewItemV2::None;
			option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItemV4::Alternate
			                   : QStyleOptionViewItemV4::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;
	}
	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() );
		QStyleOptionViewItemV4 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 QStyleOptionViewItemV4 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 if ( d->categoryDrawer )
			{
				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 QStyleOptionViewItemV4 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 QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex );
		const QPoint mousePos = viewport()->mapFromGlobal ( QCursor::pos() );
		if ( option.rect.contains ( mousePos ) )
		{
			if ( d->categoryDrawer )
			{
				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();
	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 QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex );
		const QPoint mousePos = viewport()->mapFromGlobal ( QCursor::pos() );
		if ( option.rect.contains ( mousePos ) )
		{
			if ( d->categoryDrawer )
			{
				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 QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex );
		d->categoryDrawer->mouseLeft ( categoryIndex, option.rect );
		*d->hoveredBlock = Private::Block();
		d->hoveredCategory = QString();
		viewport()->update ( option.rect );
	}
}

void KCategorizedView::startDrag ( Qt::DropActions supportedActions )
{
	QListView::startDrag ( supportedActions );
}

void KCategorizedView::dragMoveEvent ( QDragMoveEvent *event )
{
	QListView::dragMoveEvent ( event );
	d->hoveredIndex = indexAt ( event->pos() );
}

void KCategorizedView::dragEnterEvent ( QDragEnterEvent *event )
{
	QListView::dragEnterEvent ( event );
}

void KCategorizedView::dragLeaveEvent ( QDragLeaveEvent *event )
{
	QListView::dragLeaveEvent ( event );
}

void KCategorizedView::dropEvent ( QDropEvent *event )
{
	QListView::dropEvent ( event );
}

//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() || uniformItemWidths() )
		{
			const QModelIndex current = currentIndex();
			const QSize itemSize = d->hasGrid() ? gridSize()
			                       : sizeHintForIndex ( current );
			const Private::Block &block = d->blocks[d->categoryForIndex ( current )];
			//HACK: Why is the -2 needed?
			const int maxItemsPerRow = qMax ( ( d->viewportWidth() - spacing() - 2 ) / ( itemSize.width() + spacing() ), 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() || uniformItemWidths() )
		{
			const QModelIndex current = currentIndex();
			const QSize itemSize = d->hasGrid() ? gridSize()
			                       : sizeHintForIndex ( current );
			const Private::Block &block = d->blocks[d->categoryForIndex ( current )];
			//HACK: Why is the -2 needed?
			const int maxItemsPerRow = qMax ( ( d->viewportWidth() - spacing() - 2 ) / ( itemSize.width() + spacing() ), 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::currentChanged ( const QModelIndex &current,
                                        const QModelIndex &previous )
{
	QListView::currentChanged ( current, previous );
}

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

void KCategorizedView::slotCollapseOrExpandClicked ( QModelIndex idx )
{
	d->_k_slotCollapseOrExpandClicked ( idx );
}


#include "kcategorizedview.moc"