+ if (!m_autoScrollTimer) {
+ return;
+ }
+
+ int pos = 0;
+ int visibleSize = 0;
+ if (scrollOrientation() == Qt::Vertical) {
+ pos = m_mousePos.y();
+ visibleSize = size().height();
+ } else {
+ pos = m_mousePos.x();
+ visibleSize = size().width();
+ }
+
+ if (m_autoScrollTimer->interval() == InitialAutoScrollDelay) {
+ m_autoScrollIncrement = 0;
+ }
+
+ m_autoScrollIncrement = calculateAutoScrollingIncrement(pos, visibleSize, m_autoScrollIncrement);
+ if (m_autoScrollIncrement == 0) {
+ // The mouse position is not above an autoscroll margin (the autoscroll timer
+ // will be restarted in mouseMoveEvent())
+ m_autoScrollTimer->stop();
+ return;
+ }
+
+ if (m_rubberBand->isActive() && m_skipAutoScrollForRubberBand) {
+ // If a rubberband selection is ongoing the autoscrolling may only get triggered
+ // if the direction of the rubberband is similar to the autoscroll direction. This
+ // prevents that starting to create a rubberband within the autoscroll margins starts
+ // an autoscrolling.
+
+ const qreal minDiff = 4; // Ignore any autoscrolling if the rubberband is very small
+ const qreal diff = (scrollOrientation() == Qt::Vertical) ? m_rubberBand->endPosition().y() - m_rubberBand->startPosition().y()
+ : m_rubberBand->endPosition().x() - m_rubberBand->startPosition().x();
+ if (qAbs(diff) < minDiff || (m_autoScrollIncrement < 0 && diff > 0) || (m_autoScrollIncrement > 0 && diff < 0)) {
+ // The rubberband direction is different from the scroll direction (e.g. the rubberband has
+ // been moved up although the autoscroll direction might be down)
+ m_autoScrollTimer->stop();
+ return;
+ }
+ }
+
+ // As soon as the autoscrolling has been triggered at least once despite having an active rubberband,
+ // the autoscrolling may not get skipped anymore until a new rubberband is created
+ m_skipAutoScrollForRubberBand = false;
+
+ const qreal maxVisibleOffset = qMax(qreal(0), maximumScrollOffset() - visibleSize);
+ const qreal newScrollOffset = qMin(scrollOffset() + m_autoScrollIncrement, maxVisibleOffset);
+ setScrollOffset(newScrollOffset);
+
+ // Trigger the autoscroll timer which will periodically call
+ // triggerAutoScrolling()
+ m_autoScrollTimer->start(RepeatingAutoScrollDelay);
+}
+
+void KItemListView::slotGeometryOfGroupHeaderParentChanged()
+{
+ KItemListWidget *widget = qobject_cast<KItemListWidget *>(sender());
+ Q_ASSERT(widget);
+ KItemListGroupHeader *groupHeader = m_visibleGroups.value(widget);
+ Q_ASSERT(groupHeader);
+ updateGroupHeaderLayout(widget);
+}
+
+void KItemListView::slotRoleEditingCanceled(int index, const QByteArray &role, const QVariant &value)
+{
+ disconnectRoleEditingSignals(index);
+
+ m_editingRole = false;
+ Q_EMIT roleEditingCanceled(index, role, value);
+}
+
+void KItemListView::slotRoleEditingFinished(int index, const QByteArray &role, const QVariant &value)
+{
+ disconnectRoleEditingSignals(index);
+
+ m_editingRole = false;
+ Q_EMIT roleEditingFinished(index, role, value);
+}
+
+void KItemListView::setController(KItemListController *controller)
+{
+ if (m_controller != controller) {
+ KItemListController *previous = m_controller;
+ if (previous) {
+ KItemListSelectionManager *selectionManager = previous->selectionManager();
+ disconnect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged);
+ disconnect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged);
+ }
+
+ m_controller = controller;
+
+ if (controller) {
+ KItemListSelectionManager *selectionManager = controller->selectionManager();
+ connect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged);
+ connect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged);
+ }
+
+ onControllerChanged(controller, previous);
+ }
+}
+
+void KItemListView::setModel(KItemModelBase *model)
+{
+ if (m_model == model) {
+ return;
+ }
+
+ KItemModelBase *previous = m_model;
+
+ if (m_model) {
+ disconnect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged);
+ disconnect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted);
+ disconnect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved);
+ disconnect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved);
+ disconnect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged);
+ disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged);
+ disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged);
+ disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged);
+
+ m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count()));
+ }
+
+ m_model = model;
+ m_layouter->setModel(model);
+ m_grouped = model->groupedSorting();
+
+ if (m_model) {
+ connect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged);
+ connect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted);
+ connect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved);
+ connect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved);
+ connect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged);
+ connect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged);
+ connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged);
+ connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged);
+
+ const int itemCount = m_model->count();
+ if (itemCount > 0) {
+ slotItemsInserted(KItemRangeList() << KItemRange(0, itemCount));
+ }
+ }
+
+ onModelChanged(model, previous);
+}
+
+KItemListRubberBand *KItemListView::rubberBand() const
+{
+ return m_rubberBand;
+}
+
+void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount)
+{
+ if (m_activeTransactions > 0) {
+ if (hint == NoAnimation) {
+ // As soon as at least one property change should be done without animation,
+ // the whole transaction will be marked as not animated.
+ m_endTransactionAnimationHint = NoAnimation;
+ }
+ return;
+ }
+
+ if (!m_model || m_model->count() < 0) {
+ return;
+ }
+
+ int firstVisibleIndex = m_layouter->firstVisibleIndex();
+ if (firstVisibleIndex < 0) {
+ emitOffsetChanges();
+ return;
+ }
+
+ // Do a sanity check of the scroll-offset property: When properties of the itemlist-view have been changed
+ // it might be possible that the maximum offset got changed too. Assure that the full visible range
+ // is still shown if the maximum offset got decreased.
+ const qreal visibleOffsetRange = (scrollOrientation() == Qt::Horizontal) ? size().width() : size().height();
+ const qreal maxOffsetToShowFullRange = maximumScrollOffset() - visibleOffsetRange;
+ if (scrollOffset() > maxOffsetToShowFullRange) {
+ m_layouter->setScrollOffset(qMax(qreal(0), maxOffsetToShowFullRange));
+ firstVisibleIndex = m_layouter->firstVisibleIndex();
+ }
+
+ const int lastVisibleIndex = m_layouter->lastVisibleIndex();
+
+ int firstSibblingIndex = -1;
+ int lastSibblingIndex = -1;
+ const bool supportsExpanding = supportsItemExpanding();
+
+ QList<int> reusableItems = recycleInvisibleItems(firstVisibleIndex, lastVisibleIndex, hint);
+
+ // Assure that for each visible item a KItemListWidget is available. KItemListWidget
+ // instances from invisible items are reused. If no reusable items are
+ // found then new KItemListWidget instances get created.
+ const bool animate = (hint == Animation);
+ for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i) {
+ bool applyNewPos = true;
+
+ const QRectF itemBounds = m_layouter->itemRect(i);
+ const QPointF newPos = itemBounds.topLeft();
+ KItemListWidget *widget = m_visibleItems.value(i);
+ if (!widget) {
+ if (!reusableItems.isEmpty()) {
+ // Reuse a KItemListWidget instance from an invisible item
+ const int oldIndex = reusableItems.takeLast();
+ widget = m_visibleItems.value(oldIndex);
+ setWidgetIndex(widget, i);
+ updateWidgetProperties(widget, i);
+ initializeItemListWidget(widget);
+ } else {
+ // No reusable KItemListWidget instance is available, create a new one
+ widget = createWidget(i);
+ }
+ widget->resize(itemBounds.size());
+
+ if (animate && changedCount < 0) {
+ // Items have been deleted.
+ if (i >= changedIndex) {
+ // The item is located behind the removed range. Move the
+ // created item to the imaginary old position outside the
+ // view. It will get animated to the new position later.
+ const int previousIndex = i - changedCount;
+ const QRectF itemRect = m_layouter->itemRect(previousIndex);
+ if (itemRect.isEmpty()) {
+ const QPointF invisibleOldPos = (scrollOrientation() == Qt::Vertical) ? QPointF(0, size().height()) : QPointF(size().width(), 0);
+ widget->setPos(invisibleOldPos);
+ } else {
+ widget->setPos(itemRect.topLeft());
+ }
+ applyNewPos = false;
+ }
+ }
+
+ if (supportsExpanding && changedCount == 0) {
+ if (firstSibblingIndex < 0) {
+ firstSibblingIndex = i;
+ }
+ lastSibblingIndex = i;
+ }
+ }
+
+ if (animate) {
+ if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) {
+ m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos);
+ applyNewPos = false;
+ }
+
+ const bool itemsRemoved = (changedCount < 0);
+ const bool itemsInserted = (changedCount > 0);
+ if (itemsRemoved && (i >= changedIndex)) {
+ // The item is located after the removed items. Animate the moving of the position.
+ applyNewPos = !moveWidget(widget, newPos);
+ } else if (itemsInserted && i >= changedIndex) {
+ // The item is located after the first inserted item
+ if (i <= changedIndex + changedCount - 1) {
+ // The item is an inserted item. Animate the appearing of the item.
+ // For performance reasons no animation is done when changedCount is equal
+ // to all available items.
+ if (changedCount < m_model->count()) {
+ m_animation->start(widget, KItemListViewAnimation::CreateAnimation);
+ }
+ } else if (!m_animation->isStarted(widget, KItemListViewAnimation::CreateAnimation)) {
+ // The item was already there before, so animate the moving of the position.
+ // No moving animation is done if the item is animated by a create animation: This
+ // prevents a "move animation mess" when inserting several ranges in parallel.
+ applyNewPos = !moveWidget(widget, newPos);
+ }
+ }
+ } else {
+ m_animation->stop(widget);
+ }
+
+ if (applyNewPos) {
+ widget->setPos(newPos);
+ }
+
+ Q_ASSERT(widget->index() == i);
+ widget->setVisible(true);
+
+ bool animateIconResizing = animate;
+
+ if (widget->size() != itemBounds.size()) {
+ // Resize the widget for the item to the changed size.
+ if (animate) {
+ // If a dynamic item size is used then no animation is done in the direction
+ // of the dynamic size.
+ if (m_itemSize.width() <= 0) {
+ // The width is dynamic, apply the new width without animation.
+ widget->resize(itemBounds.width(), widget->size().height());
+ } else if (m_itemSize.height() <= 0) {
+ // The height is dynamic, apply the new height without animation.
+ widget->resize(widget->size().width(), itemBounds.height());
+ }
+ m_animation->start(widget, KItemListViewAnimation::ResizeAnimation, itemBounds.size());
+ } else {
+ widget->resize(itemBounds.size());
+ }
+ } else {
+ animateIconResizing = false;
+ }
+
+ const int newIconSize = widget->styleOption().iconSize;
+ if (widget->iconSize() != newIconSize) {
+ if (animateIconResizing) {
+ m_animation->start(widget, KItemListViewAnimation::IconResizeAnimation, newIconSize);
+ } else {
+ widget->setIconSize(newIconSize);
+ }
+ }
+
+ // Updating the cell-information must be done as last step: The decision whether the
+ // moving-animation should be started at all is based on the previous cell-information.
+ const Cell cell(m_layouter->itemColumn(i), m_layouter->itemRow(i));
+ m_visibleCells.insert(i, cell);
+ }
+
+ // Delete invisible KItemListWidget instances that have not been reused
+ for (int index : std::as_const(reusableItems)) {
+ recycleWidget(m_visibleItems.value(index));
+ }
+
+ if (supportsExpanding && firstSibblingIndex >= 0) {
+ Q_ASSERT(lastSibblingIndex >= 0);
+ updateSiblingsInformation(firstSibblingIndex, lastSibblingIndex);
+ }
+
+ if (m_grouped) {
+ // Update the layout of all visible group headers
+ QHashIterator<KItemListWidget *, KItemListGroupHeader *> it(m_visibleGroups);
+ while (it.hasNext()) {
+ it.next();
+ updateGroupHeaderLayout(it.key());
+ }
+ }
+
+ emitOffsetChanges();
+}
+
+QList<int> KItemListView::recycleInvisibleItems(int firstVisibleIndex, int lastVisibleIndex, LayoutAnimationHint hint)
+{
+ // Determine all items that are completely invisible and might be
+ // reused for items that just got (at least partly) visible. If the
+ // animation hint is set to 'Animation' items that do e.g. an animated
+ // moving of their position are not marked as invisible: This assures
+ // that a scrolling inside the view can be done without breaking an animation.
+
+ QList<int> items;
+
+ QHashIterator<int, KItemListWidget *> it(m_visibleItems);
+ while (it.hasNext()) {
+ it.next();
+
+ KItemListWidget *widget = it.value();
+ const int index = widget->index();
+ const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex);
+
+ if (invisible) {
+ if (m_animation->isStarted(widget)) {
+ if (hint == NoAnimation) {
+ // Stopping the animation will call KItemListView::slotAnimationFinished()
+ // and the widget will be recycled if necessary there.
+ m_animation->stop(widget);
+ }
+ } else {
+ widget->setVisible(false);
+ items.append(index);
+
+ if (m_grouped) {
+ recycleGroupHeaderForWidget(widget);
+ }
+ }
+ }
+ }
+
+ return items;
+}
+
+bool KItemListView::moveWidget(KItemListWidget *widget, const QPointF &newPos)
+{
+ if (widget->pos() == newPos) {
+ return false;
+ }
+
+ bool startMovingAnim = false;
+
+ if (m_itemSize.isEmpty()) {
+ // The items are not aligned in a grid but either as columns or rows.
+ startMovingAnim = true;