#include <KDebug>
-#define KITEMLISTVIEWLAYOUTER_DEBUG
-
-namespace {
- // TODO
- const int HeaderHeight = 50;
-};
+// #define KITEMLISTVIEWLAYOUTER_DEBUG
KItemListViewLayouter::KItemListViewLayouter(QObject* parent) :
QObject(parent),
m_dirty(true),
m_visibleIndexesDirty(true),
- m_grouped(false),
m_scrollOrientation(Qt::Vertical),
m_size(),
m_itemSize(128, 128),
+ m_itemMargin(),
m_headerHeight(0),
m_model(0),
m_sizeHintResolver(0),
- m_offset(0),
- m_maximumOffset(0),
+ m_scrollOffset(0),
+ m_maximumScrollOffset(0),
+ m_itemOffset(0),
+ m_maximumItemOffset(0),
m_firstVisibleIndex(-1),
m_lastVisibleIndex(-1),
- m_firstVisibleGroupIndex(-1),
m_columnWidth(0),
m_xPosInc(0),
m_columnCount(0),
- m_groups(),
- m_groupIndexes(),
- m_itemBoundingRects()
+ m_groupItemIndexes(),
+ m_groupHeaderHeight(0),
+ m_itemRects()
{
}
return m_itemSize;
}
+void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
+{
+ if (m_itemMargin != margin) {
+ m_itemMargin = margin;
+ m_dirty = true;
+ }
+}
+
+QSizeF KItemListViewLayouter::itemMargin() const
+{
+ return m_itemMargin;
+}
+
void KItemListViewLayouter::setHeaderHeight(qreal height)
{
if (m_headerHeight != height) {
return m_headerHeight;
}
-void KItemListViewLayouter::setOffset(qreal offset)
+void KItemListViewLayouter::setGroupHeaderHeight(qreal height)
+{
+ if (m_groupHeaderHeight != height) {
+ m_groupHeaderHeight = height;
+ m_dirty = true;
+ }
+}
+
+qreal KItemListViewLayouter::groupHeaderHeight() const
+{
+ return m_groupHeaderHeight;
+}
+
+void KItemListViewLayouter::setScrollOffset(qreal offset)
+{
+ if (m_scrollOffset != offset) {
+ m_scrollOffset = offset;
+ m_visibleIndexesDirty = true;
+ }
+}
+
+qreal KItemListViewLayouter::scrollOffset() const
+{
+ return m_scrollOffset;
+}
+
+qreal KItemListViewLayouter::maximumScrollOffset() const
+{
+ const_cast<KItemListViewLayouter*>(this)->doLayout();
+ return m_maximumScrollOffset;
+}
+
+void KItemListViewLayouter::setItemOffset(qreal offset)
{
- if (m_offset != offset) {
- m_offset = offset;
+ if (m_itemOffset != offset) {
+ m_itemOffset = offset;
m_visibleIndexesDirty = true;
}
}
-qreal KItemListViewLayouter::offset() const
+qreal KItemListViewLayouter::itemOffset() const
{
- return m_offset;
+ return m_itemOffset;
+}
+
+qreal KItemListViewLayouter::maximumItemOffset() const
+{
+ const_cast<KItemListViewLayouter*>(this)->doLayout();
+ return m_maximumItemOffset;
}
void KItemListViewLayouter::setModel(const KItemModelBase* model)
return m_sizeHintResolver;
}
-qreal KItemListViewLayouter::maximumOffset() const
-{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
- return m_maximumOffset;
-}
-
int KItemListViewLayouter::firstVisibleIndex() const
{
const_cast<KItemListViewLayouter*>(this)->doLayout();
return m_lastVisibleIndex;
}
-QRectF KItemListViewLayouter::itemBoundingRect(int index) const
+QRectF KItemListViewLayouter::itemRect(int index) const
{
const_cast<KItemListViewLayouter*>(this)->doLayout();
- if (index < 0 || index >= m_itemBoundingRects.count()) {
+ if (index < 0 || index >= m_itemRects.count()) {
return QRectF();
}
if (m_scrollOrientation == Qt::Horizontal) {
// Rotate the logical direction which is always vertical by 90°
// to get the physical horizontal direction
- const QRectF& b = m_itemBoundingRects[index];
+ const QRectF& b = m_itemRects[index];
QRectF bounds(b.y(), b.x(), b.height(), b.width());
QPointF pos = bounds.topLeft();
- pos.rx() -= m_offset;
+ pos.rx() -= m_scrollOffset;
bounds.moveTo(pos);
return bounds;
}
- QRectF bounds = m_itemBoundingRects[index];
- QPointF pos = bounds.topLeft();
- pos.ry() -= m_offset;
- bounds.moveTo(pos);
+ QRectF bounds = m_itemRects[index];
+ bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
return bounds;
}
+QRectF KItemListViewLayouter::groupHeaderRect(int index) const
+{
+ const_cast<KItemListViewLayouter*>(this)->doLayout();
+
+ const QRectF firstItemRect = itemRect(index);
+ QPointF pos = firstItemRect.topLeft();
+ if (pos.isNull()) {
+ return QRectF();
+ }
+
+ pos.ry() -= m_groupHeaderHeight;
+
+ QSizeF size;
+ if (m_scrollOrientation == Qt::Vertical) {
+ pos.rx() = 0;
+ size = QSizeF(m_size.width(), m_groupHeaderHeight);
+ } else {
+ size = QSizeF(minimumGroupHeaderWidth(), m_groupHeaderHeight);
+ }
+ return QRectF(pos, size);
+}
+
int KItemListViewLayouter::maximumVisibleItems() const
{
const_cast<KItemListViewLayouter*>(this)->doLayout();
return rows * m_columnCount;
}
-int KItemListViewLayouter::itemsPerOffset() const
-{
- return m_columnCount;
-}
-
bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
{
- return m_groupIndexes.contains(itemIndex);
+ const_cast<KItemListViewLayouter*>(this)->doLayout();
+ return m_groupItemIndexes.contains(itemIndex);
}
void KItemListViewLayouter::markAsDirty()
m_visibleIndexesDirty = true;
QSizeF itemSize = m_itemSize;
+ QSizeF itemMargin = m_itemMargin;
QSizeF size = m_size;
+
+ const bool grouped = createGroupHeaders();
const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
if (horizontalScrolling) {
+ // Flip everything so that the layout logically can work like having
+ // a vertical scrolling
itemSize.setWidth(m_itemSize.height());
itemSize.setHeight(m_itemSize.width());
+ itemMargin.setWidth(m_itemMargin.height());
+ itemMargin.setHeight(m_itemMargin.width());
size.setWidth(m_size.height());
size.setHeight(m_size.width());
+
+ if (grouped) {
+ // In the horizontal scrolling case all groups are aligned
+ // at the top, which decreases the available height. For the
+ // flipped data this means that the width must be decreased.
+ size.rwidth() -= m_groupHeaderHeight;
+ }
}
- m_columnWidth = itemSize.width();
- m_columnCount = qMax(1, int(size.width() / m_columnWidth));
- m_xPosInc = 0;
+ m_columnWidth = itemSize.width() + itemMargin.width();
+ const qreal widthForColumns = size.width() - itemMargin.width();
+ m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
+ m_xPosInc = itemMargin.width();
const int itemCount = m_model->count();
- if (itemCount > m_columnCount) {
+ if (itemCount > m_columnCount && m_columnWidth >= 32) {
// Apply the unused width equally to each column
const qreal unusedWidth = size.width() - m_columnCount * m_columnWidth;
- const qreal columnInc = unusedWidth / (m_columnCount + 1);
- m_columnWidth += columnInc;
- m_xPosInc += columnInc;
+ if (unusedWidth > 0) {
+ const qreal columnInc = unusedWidth / (m_columnCount + 1);
+ m_columnWidth += columnInc;
+ m_xPosInc += columnInc;
+ }
}
int rowCount = itemCount / m_columnCount;
++rowCount;
}
- m_itemBoundingRects.reserve(itemCount);
+ m_itemRects.reserve(itemCount);
- qreal y = m_headerHeight;
+ qreal y = m_headerHeight + itemMargin.height();
int rowIndex = 0;
int index = 0;
qreal x = m_xPosInc;
qreal maxItemHeight = itemSize.height();
+ if (grouped) {
+ if (horizontalScrolling) {
+ // All group headers will always be aligned on the top and not
+ // flipped like the other properties
+ x += m_groupHeaderHeight;
+ }
+
+ if (m_groupItemIndexes.contains(index)) {
+ if (!horizontalScrolling) {
+ // The item is the first item of a group.
+ // Increase the y-position to provide space
+ // for the group header.
+ y += m_groupHeaderHeight;
+ }
+ }
+ }
+
int column = 0;
while (index < itemCount && column < m_columnCount) {
qreal requiredItemHeight = itemSize.height();
}
const QRectF bounds(x, y, itemSize.width(), requiredItemHeight);
- if (index < m_itemBoundingRects.count()) {
- m_itemBoundingRects[index] = bounds;
+ if (index < m_itemRects.count()) {
+ m_itemRects[index] = bounds;
} else {
- m_itemBoundingRects.append(bounds);
+ m_itemRects.append(bounds);
+ }
+
+ if (grouped && horizontalScrolling) {
+ // When grouping is enabled in the horizontal mode, the header alignment
+ // looks like this:
+ // Header-1 Header-2 Header-3
+ // Item 1 Item 4 Item 7
+ // Item 2 Item 5 Item 8
+ // Item 3 Item 6 Item 9
+ // In this case 'requiredItemHeight' represents the column-width. We don't
+ // check the content of the header in the layouter to determine the required
+ // width, hence assure that at least a minimal width of 15 characters is given
+ // (in average a character requires the halve width of the font height).
+ //
+ // TODO: Let the group headers provide a minimum width and respect this width here
+ const qreal headerWidth = minimumGroupHeaderWidth();
+ if (requiredItemHeight < headerWidth) {
+ requiredItemHeight = headerWidth;
+ }
}
maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
x += m_columnWidth;
++index;
++column;
+
+ if (grouped && m_groupItemIndexes.contains(index)) {
+ // The item represents the first index of a group
+ // and must aligned in the first column
+ break;
+ }
}
- y += maxItemHeight;
+ y += maxItemHeight + itemMargin.height();
++rowIndex;
}
- if (m_itemBoundingRects.count() > itemCount) {
- m_itemBoundingRects.erase(m_itemBoundingRects.begin() + itemCount,
- m_itemBoundingRects.end());
+ if (m_itemRects.count() > itemCount) {
+ m_itemRects.erase(m_itemRects.begin() + itemCount,
+ m_itemRects.end());
}
- m_maximumOffset = (itemCount > 0) ? m_itemBoundingRects.last().bottom() : 0;
+ if (itemCount > 0) {
+ // Calculate the maximum y-range of the last row for m_maximumScrollOffset
+ m_maximumScrollOffset = m_itemRects.last().bottom();
+ const qreal rowY = m_itemRects.last().y();
- m_grouped = !m_model->groupRole().isEmpty();
- /*if (m_grouped) {
- createGroupHeaders();
-
- const int lastGroupItemCount = m_model->count() - m_groups.last().firstItemIndex;
- m_maximumOffset = m_groups.last().y + (lastGroupItemCount / m_columnCount) * m_rowHeight;
- if (lastGroupItemCount % m_columnCount != 0) {
- m_maximumOffset += m_rowHeight;
+ int index = m_itemRects.count() - 2;
+ while (index >= 0 && m_itemRects.at(index).bottom() >= rowY) {
+ m_maximumScrollOffset = qMax(m_maximumScrollOffset, m_itemRects.at(index).bottom());
+ --index;
}
- } else {*/
- // m_maximumOffset = m_minimumRowHeight * rowCount;
- //}
+
+ m_maximumItemOffset = m_columnCount * m_columnWidth;
+ } else {
+ m_maximumScrollOffset = 0;
+ m_maximumItemOffset = 0;
+ }
#ifdef KITEMLISTVIEWLAYOUTER_DEBUG
kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
m_dirty = false;
}
- if (m_grouped) {
- updateGroupedVisibleIndexes();
- } else {
- updateVisibleIndexes();
- }
+ updateVisibleIndexes();
}
void KItemListViewLayouter::updateVisibleIndexes()
return;
}
- Q_ASSERT(!m_grouped);
Q_ASSERT(!m_dirty);
if (m_model->count() <= 0) {
return;
}
- const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal);
- const int minimumHeight = horizontalScrolling ? m_itemSize.width()
- : m_itemSize.height();
-
- // Calculate the first visible index:
- // 1. Guess the index by using the minimum row height
const int maxIndex = m_model->count() - 1;
- m_firstVisibleIndex = int(m_offset / minimumHeight) * m_columnCount;
- // 2. Decrease the index by checking the real row heights
- int prevRowIndex = m_firstVisibleIndex - m_columnCount;
- while (prevRowIndex > maxIndex) {
- prevRowIndex -= m_columnCount;
- }
+ // Calculate the first visible index that is fully visible
+ int min = 0;
+ int max = maxIndex;
+ int mid = 0;
+ do {
+ mid = (min + max) / 2;
+ if (m_itemRects[mid].top() < m_scrollOffset) {
+ min = mid + 1;
+ } else {
+ max = mid - 1;
+ }
+ } while (min <= max);
+
+ if (mid > 0) {
+ // Include the row before the first fully visible index, as it might
+ // be partly visible
+ if (m_itemRects[mid].top() >= m_scrollOffset) {
+ --mid;
+ Q_ASSERT(m_itemRects[mid].top() < m_scrollOffset);
+ }
- while (prevRowIndex >= 0 && m_itemBoundingRects[prevRowIndex].bottom() >= m_offset) {
- m_firstVisibleIndex = prevRowIndex;
- prevRowIndex -= m_columnCount;
- }
- m_firstVisibleIndex = qBound(0, m_firstVisibleIndex, maxIndex);
-
- // Calculate the last visible index
- const int visibleHeight = horizontalScrolling ? m_size.width() : m_size.height();
- const qreal bottom = m_offset + visibleHeight;
- m_lastVisibleIndex = m_firstVisibleIndex; // first visible row, first column
- int nextRowIndex = m_lastVisibleIndex + m_columnCount;
- while (nextRowIndex <= maxIndex && m_itemBoundingRects[nextRowIndex].y() <= bottom) {
- m_lastVisibleIndex = nextRowIndex;
- nextRowIndex += m_columnCount;
+ const qreal rowTop = m_itemRects[mid].top();
+ while (mid > 0 && m_itemRects[mid - 1].top() == rowTop) {
+ --mid;
+ }
}
- m_lastVisibleIndex += m_columnCount - 1; // move it to the last column
- m_lastVisibleIndex = qBound(0, m_lastVisibleIndex, maxIndex);
-
- m_visibleIndexesDirty = false;
-}
+ m_firstVisibleIndex = mid;
-void KItemListViewLayouter::updateGroupedVisibleIndexes()
-{
- if (!m_visibleIndexesDirty) {
- return;
+ // Calculate the last visible index that is (at least partly) visible
+ const int visibleHeight = (m_scrollOrientation == Qt::Horizontal) ? m_size.width() : m_size.height();
+ qreal bottom = m_scrollOffset + visibleHeight;
+ if (m_model->groupedSorting()) {
+ bottom += m_groupHeaderHeight;
}
- Q_ASSERT(m_grouped);
- Q_ASSERT(!m_dirty);
+ min = m_firstVisibleIndex;
+ max = maxIndex;
+ do {
+ mid = (min + max) / 2;
+ if (m_itemRects[mid].y() <= bottom) {
+ min = mid + 1;
+ } else {
+ max = mid - 1;
+ }
+ } while (min <= max);
- if (m_model->count() <= 0) {
- m_firstVisibleIndex = -1;
- m_lastVisibleIndex = -1;
- m_visibleIndexesDirty = false;
- return;
+ while (mid > 0 && m_itemRects[mid].y() > bottom) {
+ --mid;
}
+ m_lastVisibleIndex = mid;
- // Find the first visible group
- const int lastGroupIndex = m_groups.count() - 1;
- int groupIndex = lastGroupIndex;
- for (int i = 1; i < m_groups.count(); ++i) {
- if (m_groups[i].y >= m_offset) {
- groupIndex = i - 1;
- break;
- }
- }
+ m_visibleIndexesDirty = false;
+}
- // Calculate the first visible index
- qreal groupY = m_groups[groupIndex].y;
- m_firstVisibleIndex = m_groups[groupIndex].firstItemIndex;
- const int invisibleRowCount = int(m_offset - groupY) / int(m_itemSize.height());
- m_firstVisibleIndex += invisibleRowCount * m_columnCount;
- if (groupIndex + 1 <= lastGroupIndex) {
- // Check whether the calculated first visible index remains inside the current
- // group. If this is not the case let the first element of the next group be the first
- // visible index.
- const int nextGroupIndex = m_groups[groupIndex + 1].firstItemIndex;
- if (m_firstVisibleIndex > nextGroupIndex) {
- m_firstVisibleIndex = nextGroupIndex;
- }
+bool KItemListViewLayouter::createGroupHeaders()
+{
+ if (!m_model->groupedSorting()) {
+ return false;
}
- m_firstVisibleGroupIndex = groupIndex;
-
- const int maxIndex = m_model->count() - 1;
- m_firstVisibleIndex = qBound(0, m_firstVisibleIndex, maxIndex);
+ m_groupItemIndexes.clear();
- // Calculate the last visible index: Find group where the last visible item is shown.
- const qreal visibleBottom = m_offset + m_size.height(); // TODO: respect Qt::Horizontal alignment
- while ((groupIndex < lastGroupIndex) && (m_groups[groupIndex + 1].y < visibleBottom)) {
- ++groupIndex;
+ const QList<QPair<int, QVariant> > groups = m_model->groups();
+ if (groups.isEmpty()) {
+ return false;
}
- groupY = m_groups[groupIndex].y;
- m_lastVisibleIndex = m_groups[groupIndex].firstItemIndex;
- const int availableHeight = static_cast<int>(visibleBottom - groupY);
- int visibleRowCount = availableHeight / int(m_itemSize.height());
- if (availableHeight % int(m_itemSize.height()) != 0) {
- ++visibleRowCount;
+ for (int i = 0; i < groups.count(); ++i) {
+ const int firstItemIndex = groups.at(i).first;
+ m_groupItemIndexes.insert(firstItemIndex);
}
- m_lastVisibleIndex += visibleRowCount * m_columnCount - 1;
-
- if (groupIndex + 1 <= lastGroupIndex) {
- // Check whether the calculate last visible index remains inside the current group.
- // If this is not the case let the last element of this group be the last visible index.
- const int nextGroupIndex = m_groups[groupIndex + 1].firstItemIndex;
- if (m_lastVisibleIndex >= nextGroupIndex) {
- m_lastVisibleIndex = nextGroupIndex - 1;
- }
- }
- //Q_ASSERT(m_lastVisibleIndex < m_model->count());
- m_lastVisibleIndex = qBound(0, m_lastVisibleIndex, maxIndex);
- m_visibleIndexesDirty = false;
+ return true;
}
-void KItemListViewLayouter::createGroupHeaders()
+qreal KItemListViewLayouter::minimumGroupHeaderWidth() const
{
- m_groups.clear();
- m_groupIndexes.clear();
-
- // TODO:
- QList<int> numbers;
- numbers << 0 << 5 << 6 << 13 << 20 << 25 << 30 << 35 << 50;
-
- qreal y = 0;
- for (int i = 0; i < numbers.count(); ++i) {
- if (i > 0) {
- const int previousGroupItemCount = numbers[i] - m_groups.last().firstItemIndex;
- int previousGroupRowCount = previousGroupItemCount / m_columnCount;
- if (previousGroupItemCount % m_columnCount != 0) {
- ++previousGroupRowCount;
- }
- const qreal previousGroupHeight = previousGroupRowCount * m_itemSize.height();
- y += previousGroupHeight;
- }
- y += HeaderHeight;
-
- ItemGroup itemGroup;
- itemGroup.firstItemIndex = numbers[i];
- itemGroup.y = y;
-
- m_groups.append(itemGroup);
- m_groupIndexes.insert(itemGroup.firstItemIndex);
- }
+ return m_groupHeaderHeight * 15 / 2;
}
#include "kitemlistviewlayouter_p.moc"