+ painter->restore();
+ }
+
+ if (m_rubberBand->isActive()) {
+ QRectF rubberBandRect = QRectF(m_rubberBand->startPosition(), m_rubberBand->endPosition()).normalized();
+
+ const QPointF topLeft = rubberBandRect.topLeft();
+ if (scrollOrientation() == Qt::Vertical) {
+ rubberBandRect.moveTo(topLeft.x(), topLeft.y() - scrollOffset());
+ } else {
+ rubberBandRect.moveTo(topLeft.x() - scrollOffset(), topLeft.y());
+ }
+
+ QStyleOptionRubberBand opt;
+ initStyleOption(&opt);
+ opt.shape = QRubberBand::Rectangle;
+ opt.opaque = false;
+ opt.rect = rubberBandRect.toRect();
+ style()->drawControl(QStyle::CE_RubberBand, &opt, painter);
+ }
+
+ if (m_tapAndHoldIndicator->isActive()) {
+ const QPointF indicatorSize = m_tapAndHoldIndicator->endPosition();
+ const QRectF rubberBandRect =
+ QRectF(m_tapAndHoldIndicator->startPosition() - indicatorSize, (m_tapAndHoldIndicator->startPosition()) + indicatorSize).normalized();
+ QStyleOptionRubberBand opt;
+ initStyleOption(&opt);
+ opt.shape = QRubberBand::Rectangle;
+ opt.opaque = false;
+ opt.rect = rubberBandRect.toRect();
+ style()->drawControl(QStyle::CE_RubberBand, &opt, painter);
+ }
+
+ if (!m_dropIndicator.isEmpty()) {
+ const QRectF r = m_dropIndicator.toRect();
+
+ QColor color = palette().brush(QPalette::Normal, QPalette::Text).color();
+ painter->setPen(color);
+
+ // TODO: The following implementation works only for a vertical scroll-orientation
+ // and assumes a height of the m_draggingInsertIndicator of 1.
+ Q_ASSERT(r.height() == 1);
+ painter->drawLine(r.left() + 1, r.top(), r.right() - 1, r.top());
+
+ color.setAlpha(128);
+ painter->setPen(color);
+ painter->drawRect(r.left(), r.top() - 1, r.width() - 1, 2);
+ }
+}
+
+QVariant KItemListView::itemChange(GraphicsItemChange change, const QVariant &value)
+{
+ if (change == QGraphicsItem::ItemSceneHasChanged && scene()) {
+ if (!scene()->views().isEmpty()) {
+ m_styleOption.palette = scene()->views().at(0)->palette();
+ }
+ }
+ return QGraphicsItem::itemChange(change, value);
+}
+
+void KItemListView::setItemSize(const QSizeF &size)
+{
+ const QSizeF previousSize = m_itemSize;
+ if (size == previousSize) {
+ return;
+ }
+
+ // Skip animations when the number of rows or columns
+ // are changed in the grid layout. Although the animation
+ // engine can handle this usecase, it looks obtrusive.
+ const bool animate = !changesItemGridLayout(m_layouter->size(), size, m_layouter->itemMargin());
+
+ const bool alternateBackgroundsChanged = m_alternateBackgrounds && ((m_itemSize.isEmpty() && !size.isEmpty()) || (!m_itemSize.isEmpty() && size.isEmpty()));
+
+ m_itemSize = size;
+
+ if (alternateBackgroundsChanged) {
+ // For an empty item size alternate backgrounds are drawn if more than
+ // one role is shown. Assure that the backgrounds for visible items are
+ // updated when changing the size in this context.
+ updateAlternateBackgrounds();
+ }
+
+ if (size.isEmpty()) {
+ if (m_headerWidget->automaticColumnResizing()) {
+ updatePreferredColumnWidths();
+ } else {
+ // Only apply the changed height and respect the header widths
+ // set by the user
+ const qreal currentWidth = m_layouter->itemSize().width();
+ const QSizeF newSize(currentWidth, size.height());
+ m_layouter->setItemSize(newSize);
+ }
+ } else {
+ m_layouter->setItemSize(size);
+ }
+
+ m_sizeHintResolver->clearCache();
+ doLayout(animate ? Animation : NoAnimation);
+ onItemSizeChanged(size, previousSize);
+}
+
+void KItemListView::setStyleOption(const KItemListStyleOption &option)
+{
+ if (m_styleOption == option) {
+ return;
+ }
+
+ const KItemListStyleOption previousOption = m_styleOption;
+ m_styleOption = option;
+
+ bool animate = true;
+ const QSizeF margin(option.horizontalMargin, option.verticalMargin);
+ if (margin != m_layouter->itemMargin()) {
+ // Skip animations when the number of rows or columns
+ // are changed in the grid layout. Although the animation
+ // engine can handle this usecase, it looks obtrusive.
+ animate = !changesItemGridLayout(m_layouter->size(), m_layouter->itemSize(), margin);
+ m_layouter->setItemMargin(margin);
+ }
+
+ if (m_grouped) {
+ updateGroupHeaderHeight();
+ }
+
+ if (animate && (previousOption.maxTextLines != option.maxTextLines || previousOption.maxTextWidth != option.maxTextWidth)) {
+ // Animating a change of the maximum text size just results in expensive
+ // temporary eliding and clipping operations and does not look good visually.
+ animate = false;
+ }
+
+ QHashIterator<int, KItemListWidget *> it(m_visibleItems);
+ while (it.hasNext()) {
+ it.next();
+ it.value()->setStyleOption(option);
+ }
+
+ m_sizeHintResolver->clearCache();
+ m_layouter->markAsDirty();
+ doLayout(animate ? Animation : NoAnimation);
+
+ if (m_itemSize.isEmpty()) {
+ updatePreferredColumnWidths();
+ }
+
+ onStyleOptionChanged(option, previousOption);
+}
+
+void KItemListView::setScrollOrientation(Qt::Orientation orientation)
+{
+ const Qt::Orientation previousOrientation = m_layouter->scrollOrientation();
+ if (orientation == previousOrientation) {
+ return;
+ }
+
+ m_layouter->setScrollOrientation(orientation);
+ m_animation->setScrollOrientation(orientation);
+ m_sizeHintResolver->clearCache();
+
+ if (m_grouped) {
+ QMutableHashIterator<KItemListWidget *, KItemListGroupHeader *> it(m_visibleGroups);
+ while (it.hasNext()) {
+ it.next();
+ it.value()->setScrollOrientation(orientation);
+ }
+ updateGroupHeaderHeight();
+ }
+
+ doLayout(NoAnimation);
+
+ onScrollOrientationChanged(orientation, previousOrientation);
+ Q_EMIT scrollOrientationChanged(orientation, previousOrientation);
+}
+
+Qt::Orientation KItemListView::scrollOrientation() const
+{
+ return m_layouter->scrollOrientation();
+}
+
+KItemListWidgetCreatorBase *KItemListView::defaultWidgetCreator() const
+{
+ return nullptr;
+}
+
+KItemListGroupHeaderCreatorBase *KItemListView::defaultGroupHeaderCreator() const
+{
+ return nullptr;
+}
+
+void KItemListView::initializeItemListWidget(KItemListWidget *item)
+{
+ Q_UNUSED(item)
+}
+
+bool KItemListView::itemSizeHintUpdateRequired(const QSet<QByteArray> &changedRoles) const
+{
+ Q_UNUSED(changedRoles)
+ return true;
+}
+
+void KItemListView::onControllerChanged(KItemListController *current, KItemListController *previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onModelChanged(KItemModelBase *current, KItemModelBase *previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onItemSizeChanged(const QSizeF ¤t, const QSizeF &previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onScrollOffsetChanged(qreal current, qreal previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onVisibleRolesChanged(const QList<QByteArray> ¤t, const QList<QByteArray> &previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onStyleOptionChanged(const KItemListStyleOption ¤t, const KItemListStyleOption &previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
+void KItemListView::onHighlightEntireRowChanged(bool highlightEntireRow)
+{
+ Q_UNUSED(highlightEntireRow)
+}
+
+void KItemListView::onSupportsItemExpandingChanged(bool supportsExpanding)
+{
+ Q_UNUSED(supportsExpanding)
+}
+
+void KItemListView::onTransactionBegin()
+{
+}
+
+void KItemListView::onTransactionEnd()
+{
+}
+
+bool KItemListView::event(QEvent *event)
+{
+ switch (event->type()) {
+ case QEvent::PaletteChange:
+ updatePalette();
+ break;
+
+ case QEvent::FontChange:
+ updateFont();
+ break;
+
+ default:
+ // Forward all other events to the controller and handle them there
+ if (!m_editingRole && m_controller && m_controller->processEvent(event, transform())) {
+ event->accept();
+ return true;
+ }
+ }
+
+ return QGraphicsWidget::event(event);
+}
+
+void KItemListView::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+ m_mousePos = transform().map(event->pos());
+ event->accept();
+}
+
+void KItemListView::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+{
+ QGraphicsWidget::mouseMoveEvent(event);
+
+ m_mousePos = transform().map(event->pos());
+ if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) {
+ m_autoScrollTimer->start(InitialAutoScrollDelay);
+ }
+}
+
+void KItemListView::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
+{
+ event->setAccepted(true);
+ setAutoScroll(true);
+}
+
+void KItemListView::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
+{
+ QGraphicsWidget::dragMoveEvent(event);
+
+ m_mousePos = transform().map(event->pos());
+ if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) {
+ m_autoScrollTimer->start(InitialAutoScrollDelay);
+ }
+}
+
+void KItemListView::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
+{
+ QGraphicsWidget::dragLeaveEvent(event);
+ setAutoScroll(false);
+}
+
+void KItemListView::dropEvent(QGraphicsSceneDragDropEvent *event)
+{
+ QGraphicsWidget::dropEvent(event);
+ setAutoScroll(false);
+}
+
+QList<KItemListWidget *> KItemListView::visibleItemListWidgets() const
+{
+ return m_visibleItems.values();
+}
+
+void KItemListView::updateFont()
+{
+ if (scene() && !scene()->views().isEmpty()) {
+ KItemListStyleOption option = styleOption();
+ option.font = scene()->views().first()->font();
+ option.fontMetrics = QFontMetrics(option.font);
+
+ setStyleOption(option);
+ }
+}
+
+void KItemListView::updatePalette()
+{
+ if (scene() && !scene()->views().isEmpty()) {
+ KItemListStyleOption option = styleOption();
+ option.palette = scene()->views().first()->palette();
+
+ setStyleOption(option);
+ }
+}
+
+void KItemListView::slotItemsInserted(const KItemRangeList &itemRanges)
+{
+ if (m_itemSize.isEmpty()) {
+ updatePreferredColumnWidths(itemRanges);
+ }
+
+ const bool hasMultipleRanges = (itemRanges.count() > 1);
+ if (hasMultipleRanges) {
+ beginTransaction();
+ }
+
+ m_layouter->markAsDirty();
+
+ m_sizeHintResolver->itemsInserted(itemRanges);
+
+ int previouslyInsertedCount = 0;
+ for (const KItemRange &range : itemRanges) {
+ // range.index is related to the model before anything has been inserted.
+ // As in each loop the current item-range gets inserted the index must
+ // be increased by the already previously inserted items.
+ const int index = range.index + previouslyInsertedCount;
+ const int count = range.count;
+ if (index < 0 || count <= 0) {
+ qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")";
+ continue;
+ }
+ previouslyInsertedCount += count;
+
+ // Determine which visible items must be moved
+ QList<int> itemsToMove;
+ QHashIterator<int, KItemListWidget *> it(m_visibleItems);
+ while (it.hasNext()) {
+ it.next();
+ const int visibleItemIndex = it.key();
+ if (visibleItemIndex >= index) {
+ itemsToMove.append(visibleItemIndex);
+ }
+ }
+
+ // Update the indexes of all KItemListWidget instances that are located
+ // after the inserted items. It is important to adjust the indexes in the order
+ // from the highest index to the lowest index to prevent overlaps when setting the new index.
+ std::sort(itemsToMove.begin(), itemsToMove.end());
+ for (int i = itemsToMove.count() - 1; i >= 0; --i) {
+ KItemListWidget *widget = m_visibleItems.value(itemsToMove[i]);
+ Q_ASSERT(widget);
+ const int newIndex = widget->index() + count;
+ if (hasMultipleRanges) {
+ setWidgetIndex(widget, newIndex);
+ } else {
+ // Try to animate the moving of the item
+ moveWidgetToIndex(widget, newIndex);
+ }
+ }
+
+ if (m_model->count() == count && m_activeTransactions == 0) {
+ // Check whether a scrollbar is required to show the inserted items. In this case
+ // the size of the layouter will be decreased before calling doLayout(): This prevents
+ // an unnecessary temporary animation due to the geometry change of the inserted scrollbar.
+ const bool verticalScrollOrientation = (scrollOrientation() == Qt::Vertical);
+ const bool decreaseLayouterSize = (verticalScrollOrientation && maximumScrollOffset() > size().height())
+ || (!verticalScrollOrientation && maximumScrollOffset() > size().width());
+ if (decreaseLayouterSize) {
+ const int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent);
+
+ int scrollbarSpacing = 0;
+ if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) {
+ scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing);
+ }
+
+ QSizeF layouterSize = m_layouter->size();
+ if (verticalScrollOrientation) {
+ layouterSize.rwidth() -= scrollBarExtent + scrollbarSpacing;
+ } else {
+ layouterSize.rheight() -= scrollBarExtent + scrollbarSpacing;
+ }
+ m_layouter->setSize(layouterSize);
+ }
+ }
+
+ if (!hasMultipleRanges) {
+ doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, count);
+ updateSiblingsInformation();
+ }
+ }
+
+ if (m_controller) {
+ m_controller->selectionManager()->itemsInserted(itemRanges);
+ }
+
+ if (hasMultipleRanges) {
+ m_endTransactionAnimationHint = NoAnimation;
+ endTransaction();
+
+ updateSiblingsInformation();
+ }
+
+ if (m_grouped && (hasMultipleRanges || itemRanges.first().count < m_model->count())) {
+ // In case if items of the same group have been inserted before an item that
+ // currently represents the first item of the group, the group header of
+ // this item must be removed.
+ updateVisibleGroupHeaders();
+ }
+
+ if (useAlternateBackgrounds()) {
+ updateAlternateBackgrounds();
+ }
+}
+
+void KItemListView::slotItemsRemoved(const KItemRangeList &itemRanges)
+{
+ if (m_itemSize.isEmpty()) {
+ // Don't pass the item-range: The preferred column-widths of
+ // all items must be adjusted when removing items.
+ updatePreferredColumnWidths();
+ }
+
+ const bool hasMultipleRanges = (itemRanges.count() > 1);
+ if (hasMultipleRanges) {
+ beginTransaction();
+ }
+
+ m_layouter->markAsDirty();
+
+ m_sizeHintResolver->itemsRemoved(itemRanges);
+
+ for (int i = itemRanges.count() - 1; i >= 0; --i) {
+ const KItemRange &range = itemRanges[i];
+ const int index = range.index;
+ const int count = range.count;
+ if (index < 0 || count <= 0) {
+ qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")";
+ continue;
+ }
+
+ const int firstRemovedIndex = index;
+ const int lastRemovedIndex = index + count - 1;
+
+ // Remember which items have to be moved because they are behind the removed range.
+ QVector<int> itemsToMove;
+
+ // Remove all KItemListWidget instances that got deleted
+ // Iterate over a const copy because the container is mutated within the loop
+ // directly and in `recycleWidget()` (https://bugs.kde.org/show_bug.cgi?id=428374)
+ const auto visibleItems = m_visibleItems;
+ for (KItemListWidget *widget : visibleItems) {
+ const int i = widget->index();
+ if (i < firstRemovedIndex) {
+ continue;
+ } else if (i > lastRemovedIndex) {
+ itemsToMove.append(i);
+ continue;
+ }
+
+ m_animation->stop(widget);
+ // Stopping the animation might lead to recycling the widget if
+ // it is invisible (see slotAnimationFinished()).
+ // Check again whether it is still visible:
+ if (!m_visibleItems.contains(i)) {
+ continue;
+ }
+
+ if (m_model->count() == 0 || hasMultipleRanges || !animateChangedItemCount(count)) {
+ // Remove the widget without animation
+ recycleWidget(widget);
+ } else {
+ // Animate the removing of the items. Special case: When removing an item there
+ // is no valid model index available anymore. For the
+ // remove-animation the item gets removed from m_visibleItems but the widget
+ // will stay alive until the animation has been finished and will
+ // be recycled (deleted) in KItemListView::slotAnimationFinished().
+ m_visibleItems.remove(i);
+ widget->setIndex(-1);
+ m_animation->start(widget, KItemListViewAnimation::DeleteAnimation);
+ }
+ }
+
+ // Update the indexes of all KItemListWidget instances that are located
+ // after the deleted items. It is important to update them in ascending
+ // order to prevent overlaps when setting the new index.
+ std::sort(itemsToMove.begin(), itemsToMove.end());
+ for (int i : qAsConst(itemsToMove)) {
+ KItemListWidget *widget = m_visibleItems.value(i);
+ Q_ASSERT(widget);
+ const int newIndex = i - count;
+ if (hasMultipleRanges) {
+ setWidgetIndex(widget, newIndex);
+ } else {
+ // Try to animate the moving of the item
+ moveWidgetToIndex(widget, newIndex);
+ }
+ }
+
+ if (!hasMultipleRanges) {
+ // The decrease-layout-size optimization in KItemListView::slotItemsInserted()
+ // assumes an updated geometry. If items are removed during an active transaction,
+ // the transaction will be temporary deactivated so that doLayout() triggers a
+ // geometry update if necessary.
+ const int activeTransactions = m_activeTransactions;
+ m_activeTransactions = 0;
+ doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, -count);
+ m_activeTransactions = activeTransactions;
+ updateSiblingsInformation();
+ }
+ }
+
+ if (m_controller) {
+ m_controller->selectionManager()->itemsRemoved(itemRanges);
+ }
+
+ if (hasMultipleRanges) {
+ m_endTransactionAnimationHint = NoAnimation;
+ endTransaction();
+ updateSiblingsInformation();
+ }
+
+ if (m_grouped && (hasMultipleRanges || m_model->count() > 0)) {
+ // In case if the first item of a group has been removed, the group header
+ // must be applied to the next visible item.
+ updateVisibleGroupHeaders();
+ }
+
+ if (useAlternateBackgrounds()) {
+ updateAlternateBackgrounds();
+ }
+}
+
+void KItemListView::slotItemsMoved(const KItemRange &itemRange, const QList<int> &movedToIndexes)
+{
+ m_sizeHintResolver->itemsMoved(itemRange, movedToIndexes);
+ m_layouter->markAsDirty();
+
+ if (m_controller) {
+ m_controller->selectionManager()->itemsMoved(itemRange, movedToIndexes);
+ }
+
+ const int firstVisibleMovedIndex = qMax(firstVisibleIndex(), itemRange.index);
+ const int lastVisibleMovedIndex = qMin(lastVisibleIndex(), itemRange.index + itemRange.count - 1);
+
+ for (int index = firstVisibleMovedIndex; index <= lastVisibleMovedIndex; ++index) {
+ KItemListWidget *widget = m_visibleItems.value(index);
+ if (widget) {
+ updateWidgetProperties(widget, index);
+ initializeItemListWidget(widget);
+ }
+ }
+
+ doLayout(NoAnimation);
+ updateSiblingsInformation();
+}
+
+void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
+{
+ const bool updateSizeHints = itemSizeHintUpdateRequired(roles);
+ if (updateSizeHints && m_itemSize.isEmpty()) {
+ updatePreferredColumnWidths(itemRanges);
+ }
+
+ for (const KItemRange &itemRange : itemRanges) {
+ const int index = itemRange.index;
+ const int count = itemRange.count;
+
+ if (updateSizeHints) {
+ m_sizeHintResolver->itemsChanged(index, count, roles);
+ m_layouter->markAsDirty();
+ }
+
+ // Apply the changed roles to the visible item-widgets
+ const int lastIndex = index + count - 1;
+ for (int i = index; i <= lastIndex; ++i) {
+ KItemListWidget *widget = m_visibleItems.value(i);
+ if (widget) {
+ widget->setData(m_model->data(i), roles);