-/***************************************************************************
- * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program; if not, write to the *
- * Free Software Foundation, Inc., *
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
- ***************************************************************************/
+/*
+ * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
#include "kitemlistviewlayouter.h"
-
-#include <kitemviews/kitemmodelbase.h>
+#include "dolphindebug.h"
#include "kitemlistsizehintresolver.h"
+#include "kitemviews/kitemmodelbase.h"
-#include <KDebug>
+#include <QGuiApplication>
+#include <QScopeGuard>
// #define KITEMLISTVIEWLAYOUTER_DEBUG
-KItemListViewLayouter::KItemListViewLayouter(QObject* parent) :
- QObject(parent),
- m_dirty(true),
- m_visibleIndexesDirty(true),
- m_scrollOrientation(Qt::Vertical),
- m_size(),
- m_itemSize(128, 128),
- m_itemMargin(),
- m_headerHeight(0),
- m_model(0),
- m_sizeHintResolver(0),
- m_scrollOffset(0),
- m_maximumScrollOffset(0),
- m_itemOffset(0),
- m_maximumItemOffset(0),
- m_firstVisibleIndex(-1),
- m_lastVisibleIndex(-1),
- m_columnWidth(0),
- m_xPosInc(0),
- m_columnCount(0),
- m_groupItemIndexes(),
- m_groupHeaderHeight(0),
- m_groupHeaderMargin(0),
- m_itemInfos()
-{
+KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver *sizeHintResolver, QObject *parent)
+ : QObject(parent)
+ , m_dirty(true)
+ , m_visibleIndexesDirty(true)
+ , m_scrollOrientation(Qt::Vertical)
+ , m_size()
+ , m_itemSize(128, 128)
+ , m_itemMargin()
+ , m_headerHeight(0)
+ , m_model(nullptr)
+ , m_sizeHintResolver(sizeHintResolver)
+ , m_scrollOffset(0)
+ , m_maximumScrollOffset(0)
+ , m_itemOffset(0)
+ , m_maximumItemOffset(0)
+ , m_firstVisibleIndex(-1)
+ , m_lastVisibleIndex(-1)
+ , m_columnWidth(0)
+ , m_xPosInc(0)
+ , m_columnCount(0)
+ , m_rowOffsets()
+ , m_columnOffsets()
+ , m_groupItemIndexes()
+ , m_groupHeaderHeight(0)
+ , m_groupHeaderMargin(0)
+ , m_itemInfos()
+ , m_statusBarOffset(0)
+{
+ Q_ASSERT(m_sizeHintResolver);
}
KItemListViewLayouter::~KItemListViewLayouter()
return m_scrollOrientation;
}
-void KItemListViewLayouter::setSize(const QSizeF& size)
+void KItemListViewLayouter::setSize(const QSizeF &size)
{
if (m_size != size) {
if (m_scrollOrientation == Qt::Vertical) {
return m_size;
}
-void KItemListViewLayouter::setItemSize(const QSizeF& size)
+void KItemListViewLayouter::setItemSize(const QSizeF &size)
{
if (m_itemSize != size) {
m_itemSize = size;
return m_itemSize;
}
-void KItemListViewLayouter::setItemMargin(const QSizeF& margin)
+void KItemListViewLayouter::setItemMargin(const QSizeF &margin)
{
if (m_itemMargin != margin) {
m_itemMargin = margin;
qreal KItemListViewLayouter::maximumScrollOffset() const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
return m_maximumScrollOffset;
}
qreal KItemListViewLayouter::maximumItemOffset() const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
return m_maximumItemOffset;
}
-void KItemListViewLayouter::setModel(const KItemModelBase* model)
+void KItemListViewLayouter::setModel(const KItemModelBase *model)
{
if (m_model != model) {
m_model = model;
}
}
-const KItemModelBase* KItemListViewLayouter::model() const
+const KItemModelBase *KItemListViewLayouter::model() const
{
return m_model;
}
-void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver)
-{
- if (m_sizeHintResolver != sizeHintResolver) {
- m_sizeHintResolver = sizeHintResolver;
- m_dirty = true;
- }
-}
-
-const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const
-{
- return m_sizeHintResolver;
-}
-
int KItemListViewLayouter::firstVisibleIndex() const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
return m_firstVisibleIndex;
}
int KItemListViewLayouter::lastVisibleIndex() const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
return m_lastVisibleIndex;
}
QRectF KItemListViewLayouter::itemRect(int index) const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
if (index < 0 || index >= m_itemInfos.count()) {
return QRectF();
}
+ QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
+
+ const qreal x = m_columnOffsets.at(m_itemInfos.at(index).column);
+ const qreal y = m_rowOffsets.at(m_itemInfos.at(index).row);
+
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_itemInfos[index].rect;
- QRectF bounds(b.y(), b.x(), b.height(), b.width());
- QPointF pos = bounds.topLeft();
- pos.rx() -= m_scrollOffset;
- bounds.moveTo(pos);
- return bounds;
+ QPointF pos(y, x);
+ sizeHint.transpose();
+ if (QGuiApplication::isRightToLeft()) {
+ pos.rx() = m_size.width() - 1 + m_scrollOffset - pos.x() - sizeHint.width();
+ } else {
+ pos.rx() -= m_scrollOffset;
+ }
+ return QRectF(pos, sizeHint);
+ }
+
+ if (sizeHint.width() <= 0) {
+ // In Details View, a size hint with negative width is used internally.
+ sizeHint.rwidth() = m_itemSize.width();
}
- QRectF bounds = m_itemInfos[index].rect;
- bounds.moveTo(bounds.topLeft() - QPointF(m_itemOffset, m_scrollOffset));
- return bounds;
+ const QPointF pos(x - m_itemOffset, y - m_scrollOffset);
+ return QRectF(pos, sizeHint);
}
QRectF KItemListViewLayouter::groupHeaderRect(int index) const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
const QRectF firstItemRect = itemRect(index);
QPointF pos = firstItemRect.topLeft();
pos.ry() -= m_groupHeaderHeight;
size = QSizeF(m_size.width(), m_groupHeaderHeight);
} else {
- pos.rx() -= m_itemMargin.width();
pos.ry() = 0;
- // Determine the maximum width used in the
- // current column. As the scroll-direction is
- // Qt::Horizontal and m_itemRects is accessed directly,
- // the logical height represents the visual width.
- qreal width = minimumGroupHeaderWidth();
- const qreal y = m_itemInfos[index].rect.y();
+ // Determine the maximum width used in the current column. As the
+ // scroll-direction is Qt::Horizontal and m_itemRects is accessed
+ // directly, the logical height represents the visual width, and
+ // the logical row represents the column.
+ qreal headerWidth = minimumGroupHeaderWidth();
+ const int row = m_itemInfos[index].row;
const int maxIndex = m_itemInfos.count() - 1;
while (index <= maxIndex) {
- QRectF bounds = m_itemInfos[index].rect;
- if (bounds.y() != y) {
+ if (m_itemInfos[index].row != row) {
break;
}
- if (bounds.height() > width) {
- width = bounds.height();
+ const qreal itemWidth =
+ (m_scrollOrientation == Qt::Vertical) ? m_sizeHintResolver->sizeHint(index).width() : m_sizeHintResolver->sizeHint(index).height();
+
+ if (itemWidth > headerWidth) {
+ headerWidth = itemWidth;
}
++index;
}
- size = QSizeF(width, m_size.height());
+ size = QSizeF(headerWidth, m_size.height());
+
+ if (QGuiApplication::isRightToLeft()) {
+ pos.setX(firstItemRect.right() + m_itemMargin.width() - size.width());
+ } else {
+ pos.rx() -= m_itemMargin.width();
+ }
}
+
return QRectF(pos, size);
}
int KItemListViewLayouter::itemColumn(int index) const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
if (index < 0 || index >= m_itemInfos.count()) {
return -1;
}
- return (m_scrollOrientation == Qt::Vertical)
- ? m_itemInfos[index].column
- : m_itemInfos[index].row;
+ return (m_scrollOrientation == Qt::Vertical) ? m_itemInfos[index].column : m_itemInfos[index].row;
}
int KItemListViewLayouter::itemRow(int index) const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
if (index < 0 || index >= m_itemInfos.count()) {
return -1;
}
- return (m_scrollOrientation == Qt::Vertical)
- ? m_itemInfos[index].row
- : m_itemInfos[index].column;
+ return (m_scrollOrientation == Qt::Vertical) ? m_itemInfos[index].row : m_itemInfos[index].column;
}
int KItemListViewLayouter::maximumVisibleItems() const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
const int height = static_cast<int>(m_size.height());
const int rowHeight = static_cast<int>(m_itemSize.height());
bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const
{
- const_cast<KItemListViewLayouter*>(this)->doLayout();
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
return m_groupItemIndexes.contains(itemIndex);
}
m_dirty = true;
}
+void KItemListViewLayouter::setStatusBarOffset(int offset)
+{
+ if (m_statusBarOffset != offset) {
+ m_statusBarOffset = offset;
+ }
+}
#ifndef QT_NO_DEBUG
- bool KItemListViewLayouter::isDirty()
- {
- return m_dirty;
- }
+bool KItemListViewLayouter::isDirty()
+{
+ return m_dirty;
+}
#endif
void KItemListViewLayouter::doLayout()
{
- if (m_dirty) {
+ // we always want to update visible indexes after performing a layout
+ auto qsg = qScopeGuard([this] {
+ updateVisibleIndexes();
+ });
+
+ if (!m_dirty) {
+ return;
+ }
+
#ifdef KITEMLISTVIEWLAYOUTER_DEBUG
- QElapsedTimer timer;
- timer.start();
+ QElapsedTimer timer;
+ timer.start();
#endif
- 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.transpose();
- itemMargin.transpose();
- size.transpose();
-
- 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_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.transpose();
+ itemMargin.transpose();
+ size.transpose();
+ size.rwidth() -= m_statusBarOffset;
+
+ 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() + 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 && m_columnWidth >= 32) {
- // Apply the unused width equally to each column
- const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
- if (unusedWidth > 0) {
- const qreal columnInc = unusedWidth / (m_columnCount + 1);
- m_columnWidth += columnInc;
- m_xPosInc += columnInc;
- }
+ const bool isRightToLeft = QGuiApplication::isRightToLeft();
+ m_columnWidth = itemSize.width() + itemMargin.width();
+ const qreal widthForColumns = std::max(size.width() - itemMargin.width(), m_columnWidth);
+ m_columnCount = qMax(1, int(widthForColumns / m_columnWidth));
+ m_xPosInc = itemMargin.width();
+
+ const int itemCount = m_model->count();
+ if (itemCount > m_columnCount && m_columnWidth >= 32) {
+ // Apply the unused width equally to each column
+ const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth;
+ if (unusedWidth > 0) {
+ const qreal columnInc = unusedWidth / (m_columnCount + 1);
+ m_columnWidth += columnInc;
+ m_xPosInc += columnInc;
}
+ }
- m_itemInfos.resize(itemCount);
-
- qreal y = m_headerHeight + itemMargin.height();
- int row = 0;
+ m_itemInfos.resize(itemCount);
- int index = 0;
- while (index < itemCount) {
- qreal x = m_xPosInc;
- qreal maxItemHeight = itemSize.height();
+ // Calculate the offset of each column, i.e., the x-coordinate where the column starts.
+ m_columnOffsets.resize(m_columnCount);
+ qreal currentOffset = isRightToLeft && !horizontalScrolling ? widthForColumns : m_xPosInc;
- 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 (grouped && horizontalScrolling) {
+ // All group headers will always be aligned on the top and not
+ // flipped like the other properties.
+ currentOffset += m_groupHeaderHeight;
+ }
- if (m_groupItemIndexes.contains(index)) {
- // The item is the first item of a group.
- // Increase the y-position to provide space
- // for the group header.
- if (index > 0) {
- // Only add a margin if there has been added another
- // group already before
- y += m_groupHeaderMargin;
- } else if (!horizontalScrolling) {
- // The first group header should be aligned on top
- y -= itemMargin.height();
- }
-
- if (!horizontalScrolling) {
- y += m_groupHeaderHeight;
- }
- }
+ if (isRightToLeft) {
+ for (int column = 0; column < m_columnCount; ++column) {
+ if (horizontalScrolling) {
+ m_columnOffsets[column] = currentOffset + column * m_columnWidth;
+ } else {
+ currentOffset -= m_columnWidth;
+ m_columnOffsets[column] = currentOffset;
}
+ }
+ } else {
+ for (int column = 0; column < m_columnCount; ++column) {
+ m_columnOffsets[column] = currentOffset;
+ currentOffset += m_columnWidth;
+ }
+ }
- int column = 0;
- while (index < itemCount && column < m_columnCount) {
- qreal requiredItemHeight = itemSize.height();
- if (m_sizeHintResolver) {
- const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
- const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height();
- if (sizeHintHeight > requiredItemHeight) {
- requiredItemHeight = sizeHintHeight;
- }
+ // Prepare the QVector which stores the y-coordinate for each new row.
+ int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount;
+ if (grouped && m_columnCount > 1) {
+ // In the worst case, a new row will be started for every group.
+ // We could calculate the exact number of rows now to prevent that we reserve
+ // too much memory, but the code required to do that might need much more
+ // memory than it would save in the average case.
+ numberOfRows += m_groupItemIndexes.count();
+ }
+ m_rowOffsets.resize(numberOfRows);
+
+ qreal y = m_headerHeight + itemMargin.height();
+ int row = 0;
+
+ int index = 0;
+ while (index < itemCount) {
+ qreal maxItemHeight = itemSize.height();
+
+ if (grouped) {
+ if (m_groupItemIndexes.contains(index)) {
+ // The item is the first item of a group.
+ // Increase the y-position to provide space
+ // for the group header.
+ if (index > 0) {
+ // Only add a margin if there has been added another
+ // group already before
+ y += m_groupHeaderMargin;
+ } else if (!horizontalScrolling) {
+ // The first group header should be aligned on top
+ y -= itemMargin.height();
}
- ItemInfo& itemInfo = m_itemInfos[index];
- itemInfo.rect = QRectF(x, y, itemSize.width(), requiredItemHeight);
- itemInfo.column = column;
- itemInfo.row = row;
-
- 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;
- }
+ if (!horizontalScrolling) {
+ y += m_groupHeaderHeight;
}
+ }
+ }
+
+ m_rowOffsets[row] = y;
- maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
- x += m_columnWidth;
- ++index;
- ++column;
+ int column = 0;
+ while (index < itemCount && column < m_columnCount) {
+ qreal requiredItemHeight = itemSize.height();
+ const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index);
+ const qreal sizeHintHeight = sizeHint.height();
+ if (sizeHintHeight > requiredItemHeight) {
+ requiredItemHeight = sizeHintHeight;
+ }
- if (grouped && m_groupItemIndexes.contains(index)) {
- // The item represents the first index of a group
- // and must aligned in the first column
- break;
+ ItemInfo &itemInfo = m_itemInfos[index];
+ itemInfo.column = column;
+ itemInfo.row = row;
+
+ 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;
}
}
- y += maxItemHeight + itemMargin.height();
- ++row;
- }
+ maxItemHeight = qMax(maxItemHeight, requiredItemHeight);
+ ++index;
+ ++column;
- if (itemCount > 0) {
- m_maximumScrollOffset = y;
- m_maximumItemOffset = m_columnCount * m_columnWidth;
- } else {
- m_maximumScrollOffset = 0;
- m_maximumItemOffset = 0;
+ if (grouped && m_groupItemIndexes.contains(index)) {
+ // The item represents the first index of a group
+ // and must aligned in the first column
+ break;
+ }
}
-#ifdef KITEMLISTVIEWLAYOUTER_DEBUG
- kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
-#endif
- m_dirty = false;
+ y += maxItemHeight + itemMargin.height();
+ ++row;
}
- updateVisibleIndexes();
+ if (itemCount > 0) {
+ m_maximumScrollOffset = y;
+ m_maximumItemOffset = m_columnCount * m_columnWidth;
+ } else {
+ m_maximumScrollOffset = 0;
+ m_maximumItemOffset = 0;
+ }
+
+#ifdef KITEMLISTVIEWLAYOUTER_DEBUG
+ qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed();
+#endif
+ m_dirty = false;
}
void KItemListViewLayouter::updateVisibleIndexes()
int mid = 0;
do {
mid = (min + max) / 2;
- if (m_itemInfos[mid].rect.top() < m_scrollOffset) {
+ if (m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset) {
min = mid + 1;
} else {
max = mid - 1;
if (mid > 0) {
// Include the row before the first fully visible index, as it might
// be partly visible
- if (m_itemInfos[mid].rect.top() >= m_scrollOffset) {
+ if (m_rowOffsets.at(m_itemInfos[mid].row) >= m_scrollOffset) {
--mid;
- Q_ASSERT(m_itemInfos[mid].rect.top() < m_scrollOffset);
+ Q_ASSERT(m_rowOffsets.at(m_itemInfos[mid].row) < m_scrollOffset);
}
- const qreal rowTop = m_itemInfos[mid].rect.top();
- while (mid > 0 && m_itemInfos[mid - 1].rect.top() == rowTop) {
+ const int firstVisibleRow = m_itemInfos[mid].row;
+ while (mid > 0 && m_itemInfos[mid - 1].row == firstVisibleRow) {
--mid;
}
}
max = maxIndex;
do {
mid = (min + max) / 2;
- if (m_itemInfos[mid].rect.y() <= bottom) {
+ if (m_rowOffsets.at(m_itemInfos[mid].row) <= bottom) {
min = mid + 1;
} else {
max = mid - 1;
}
} while (min <= max);
- while (mid > 0 && m_itemInfos[mid].rect.y() > bottom) {
+ while (mid > 0 && m_rowOffsets.at(m_itemInfos[mid].row) > bottom) {
--mid;
}
m_lastVisibleIndex = mid;
m_groupItemIndexes.clear();
- const QList<QPair<int, QVariant> > groups = m_model->groups();
+ const QList<QPair<int, QVariant>> groups = m_model->groups();
if (groups.isEmpty()) {
return false;
}
return 100;
}
-#include "kitemlistviewlayouter.moc"
+#include "moc_kitemlistviewlayouter.cpp"