diff --git a/core/libs/widgets/itemview/dcategorizedview.cpp b/core/libs/widgets/itemview/dcategorizedview.cpp index 89deb4586d..7d753db9d3 100644 --- a/core/libs/widgets/itemview/dcategorizedview.cpp +++ b/core/libs/widgets/itemview/dcategorizedview.cpp @@ -1,2009 +1,2035 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2010-01-16 * Description : Item view for listing items in a categorized fashion optionally * * Copyright (C) 2007 by Rafael Fernández López * Copyright (C) 2009-2012 by Marcel Wiesweg * Copyright (C) 2011-2020 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "dcategorizedview_p.h" // C++ includes #include // Qt includes #include #include #include // Local includes #include "dcategorizedsortfilterproxymodel.h" #include "dcategorydrawer.h" /** * NOTE: By defining DOLPHIN_DRAGANDDROP the custom drag and drop implementation of * DCategorizedView is bypassed to have a consistent drag and drop look for all * views. Hopefully transparent pixmaps for drag objects will be supported in * Qt 4.4, so that this workaround can be skipped. */ #define DOLPHIN_DRAGANDDROP namespace Digikam { DCategorizedView::Private::Private(DCategorizedView* const lv) : listView(lv), categoryDrawer(nullptr), biggestItemSize(QSize(0, 0)), mouseButtonPressed(false), rightMouseButtonPressed(false), dragLeftViewport(false), drawItemsWhileDragging(true), forcedSelectionPosition(0), proxyModel(nullptr) { } DCategorizedView::Private::~Private() { } const QModelIndexList& DCategorizedView::Private::intersectionSet(const QRect& rect) { QModelIndex index; QRect indexVisualRect; int itemHeight; intersectedIndexes.clear(); if (listView->gridSize().isEmpty()) { itemHeight = biggestItemSize.height(); } else { itemHeight = listView->gridSize().height(); } // Lets find out where we should start int top = proxyModel->rowCount() - 1; int bottom = 0; int middle = (top + bottom) / 2; while (bottom <= top) { middle = (top + bottom) / 2; index = proxyModel->index(middle, 0); indexVisualRect = visualRect(index); // We need the whole height (not only the visualRect). This will help us to update // all needed indexes correctly (ereslibre) indexVisualRect.setHeight(indexVisualRect.height() + (itemHeight - indexVisualRect.height())); if (qMax(indexVisualRect.topLeft().y(), indexVisualRect.bottomRight().y()) < qMin(rect.topLeft().y(), rect.bottomRight().y())) { bottom = middle + 1; } else { top = middle - 1; } } for (int i = middle ; i < proxyModel->rowCount() ; ++i) { index = proxyModel->index(i, 0); indexVisualRect = visualRect(index); if (rect.intersects(indexVisualRect)) { intersectedIndexes.append(index); } // If we passed next item, stop searching for hits if (qMax(rect.bottomRight().y(), rect.topLeft().y()) < qMin(indexVisualRect.topLeft().y(), indexVisualRect.bottomRight().y())) { break; } } return intersectedIndexes; } QRect DCategorizedView::Private::visualRectInViewport(const QModelIndex& index) const { if (!index.isValid()) { return QRect(); } QRect retRect; QString curCategory = elementsInfo[index.row()].category; const bool leftToRightFlow = (listView->flow() == QListView::LeftToRight); if (leftToRightFlow) { if (listView->layoutDirection() == Qt::LeftToRight) { retRect = QRect(listView->spacing(), listView->spacing() * 2 + categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); } else { retRect = QRect(listView->viewport()->width() - listView->spacing(), listView->spacing() * 2 + categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); } } else { retRect = QRect(listView->spacing(), listView->spacing() * 2 + categoryDrawer->categoryHeight(index, listView->viewOptions()), 0, 0); } int viewportWidth = listView->viewport()->width() - listView->spacing(); int itemHeight; int itemWidth; if (listView->gridSize().isEmpty() && leftToRightFlow) { itemHeight = biggestItemSize.height(); itemWidth = biggestItemSize.width(); } else if (leftToRightFlow) { itemHeight = listView->gridSize().height(); itemWidth = listView->gridSize().width(); } else if (listView->gridSize().isEmpty() && !leftToRightFlow) { itemHeight = biggestItemSize.height(); itemWidth = listView->viewport()->width() - listView->spacing() * 2; } else { itemHeight = listView->gridSize().height(); itemWidth = listView->gridSize().width() - listView->spacing() * 2; } int itemWidthPlusSeparation = listView->spacing() + itemWidth; if (!itemWidthPlusSeparation) { ++itemWidthPlusSeparation; } int elementsPerRow = viewportWidth / itemWidthPlusSeparation; if (!elementsPerRow) { ++elementsPerRow; } int column; int row; if (leftToRightFlow) { column = elementsInfo[index.row()].relativeOffsetToCategory % elementsPerRow; row = elementsInfo[index.row()].relativeOffsetToCategory / elementsPerRow; if (listView->layoutDirection() == Qt::LeftToRight) { retRect.setLeft(retRect.left() + column * listView->spacing() + column * itemWidth); } else { retRect.setLeft(retRect.right() - column * listView->spacing() - column * itemWidth - itemWidth); retRect.setRight(retRect.right() - column * listView->spacing() - column * itemWidth); } } else { elementsPerRow = 1; column = elementsInfo[index.row()].relativeOffsetToCategory % elementsPerRow; row = elementsInfo[index.row()].relativeOffsetToCategory / elementsPerRow; (void)column; // Remove clang warnings. } foreach (const QString& category, categories) { if (category == curCategory) { break; } float rows = (float) ((float) categoriesIndexes[category].count() / (float) elementsPerRow); int rowsInt = categoriesIndexes[category].count() / elementsPerRow; if (rows - trunc(rows)) { ++rowsInt; } retRect.setTop(retRect.top() + (rowsInt * itemHeight) + categoryDrawer->categoryHeight(index, listView->viewOptions()) + listView->spacing() * 2); if (listView->gridSize().isEmpty()) { retRect.setTop(retRect.top() + (rowsInt * listView->spacing())); } } if (listView->gridSize().isEmpty()) { retRect.setTop(retRect.top() + row * listView->spacing() + (row * itemHeight)); } else { retRect.setTop(retRect.top() + (row * itemHeight)); } retRect.setWidth(itemWidth); QModelIndex heightIndex = proxyModel->index(index.row(), 0); if (listView->gridSize().isEmpty()) { retRect.setHeight(listView->sizeHintForIndex(heightIndex).height()); } else { const QSize sizeHint = listView->sizeHintForIndex(heightIndex); if (sizeHint.width() < itemWidth && leftToRightFlow) { retRect.setWidth(sizeHint.width()); retRect.moveLeft(retRect.left() + (itemWidth - sizeHint.width()) / 2); } retRect.setHeight(qMin(sizeHint.height(), listView->gridSize().height())); } return retRect; } QRect DCategorizedView::Private::visualCategoryRectInViewport(const QString& category) const { QRect retRect(listView->spacing(), listView->spacing(), listView->viewport()->width() - listView->spacing() * 2, 0); if (!proxyModel || !categoryDrawer || !proxyModel->isCategorizedModel() || !proxyModel->rowCount() || !categories.contains(category)) { return QRect(); } QModelIndex index = proxyModel->index(0, 0, QModelIndex()); int viewportWidth = listView->viewport()->width() - listView->spacing(); int itemHeight; int itemWidth; if (listView->gridSize().isEmpty()) { itemHeight = biggestItemSize.height(); itemWidth = biggestItemSize.width(); } else { itemHeight = listView->gridSize().height(); itemWidth = listView->gridSize().width(); } int itemWidthPlusSeparation = listView->spacing() + itemWidth; int elementsPerRow = viewportWidth / itemWidthPlusSeparation; if (!elementsPerRow) { ++elementsPerRow; } if (listView->flow() == QListView::TopToBottom) { elementsPerRow = 1; } foreach (const QString& itCategory, categories) { if (itCategory == category) { break; } float rows = (float) ((float) categoriesIndexes[itCategory].count() / (float) elementsPerRow); int rowsInt = categoriesIndexes[itCategory].count() / elementsPerRow; if (rows - trunc(rows)) { ++rowsInt; } retRect.setTop(retRect.top() + (rowsInt * itemHeight) + categoryDrawer->categoryHeight(index, listView->viewOptions()) + listView->spacing() * 2); if (listView->gridSize().isEmpty()) { retRect.setTop(retRect.top() + (rowsInt * listView->spacing())); } } retRect.setHeight(categoryDrawer->categoryHeight(index, listView->viewOptions())); return retRect; } /** * We're sure elementsPosition doesn't contain index */ const QRect& DCategorizedView::Private::cacheIndex(const QModelIndex& index) { QRect rect = visualRectInViewport(index); QHash::iterator it = elementsPosition.insert(index.row(), rect); return *it; } /** * We're sure categoriesPosition doesn't contain category */ const QRect& DCategorizedView::Private::cacheCategory(const QString& category) { QRect rect = visualCategoryRectInViewport(category); QHash::iterator it = categoriesPosition.insert(category, rect); return *it; } const QRect& DCategorizedView::Private::cachedRectIndex(const QModelIndex& index) { QHash::const_iterator it = elementsPosition.constFind(index.row()); if (it != elementsPosition.constEnd()) // If we have it cached { // return it + return *it; } else // Otherwise, cache it { // and return it + return cacheIndex(index); } } const QRect& DCategorizedView::Private::cachedRectCategory(const QString& category) { QHash::const_iterator it = categoriesPosition.constFind(category); if (it != categoriesPosition.constEnd()) // If we have it cached { // return it + return *it; } else // Otherwise, cache it and { // return it + return cacheCategory(category); } } QRect DCategorizedView::Private::visualRect(const QModelIndex& index) { QRect retRect = cachedRectIndex(index); int dx = -listView->horizontalOffset(); int dy = -listView->verticalOffset(); retRect.adjust(dx, dy, dx, dy); return retRect; } QRect DCategorizedView::Private::categoryVisualRect(const QString& category) { QRect retRect = cachedRectCategory(category); int dx = -listView->horizontalOffset(); int dy = -listView->verticalOffset(); retRect.adjust(dx, dy, dx, dy); return retRect; } QSize DCategorizedView::Private::contentsSize() { // find the last index in the last category QModelIndex lastIndex = categoriesIndexes.isEmpty() ? QModelIndex() : proxyModel->index(categoriesIndexes[categories.last()].last(), 0); int lastItemBottom = cachedRectIndex(lastIndex).top() + listView->spacing() + (listView->gridSize().isEmpty() ? biggestItemSize.height() : listView->gridSize().height()) - listView->viewport()->height(); return QSize(listView->viewport()->width(), lastItemBottom); } void DCategorizedView::Private::drawNewCategory(const QModelIndex& index, int sortRole, const QStyleOption& option, QPainter* painter) { if (!index.isValid()) { return; } QStyleOption optionCopy = option; const QString category = proxyModel->data(index, DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); optionCopy.state &= ~QStyle::State_Selected; if ((listView->selectionMode() != SingleSelection) && (listView->selectionMode() != NoSelection)) { - if ((category == hoveredCategory) && !mouseButtonPressed) + if ((category == hoveredCategory) && !mouseButtonPressed) { optionCopy.state |= QStyle::State_MouseOver; } else if ((category == hoveredCategory) && mouseButtonPressed) { QPoint initialPressPosition = listView->viewport()->mapFromGlobal(QCursor::pos()); initialPressPosition.setY(initialPressPosition.y() + listView->verticalOffset()); initialPressPosition.setX(initialPressPosition.x() + listView->horizontalOffset()); if (initialPressPosition == this->initialPressPosition) { optionCopy.state |= QStyle::State_Selected; } } } categoryDrawer->drawCategory(index, sortRole, optionCopy, painter); } void DCategorizedView::Private::updateScrollbars() { listView->horizontalScrollBar()->setRange(0, 0); if (listView->verticalScrollMode() == QAbstractItemView::ScrollPerItem) { listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); } if (listView->horizontalScrollMode() == QAbstractItemView::ScrollPerItem) { listView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); } listView->verticalScrollBar()->setSingleStep(listView->viewport()->height() / 10); listView->verticalScrollBar()->setPageStep(listView->viewport()->height()); listView->verticalScrollBar()->setRange(0, contentsSize().height()); } void DCategorizedView::Private::drawDraggedItems(QPainter* painter) { QStyleOptionViewItem option = listView->viewOptions(); option.state &= ~QStyle::State_MouseOver; foreach (const QModelIndex& index, listView->selectionModel()->selectedIndexes()) { const int dx = mousePosition.x() - initialPressPosition.x() + listView->horizontalOffset(); const int dy = mousePosition.y() - initialPressPosition.y() + listView->verticalOffset(); option.rect = visualRect(index); option.rect.adjust(dx, dy, dx, dy); if (option.rect.intersects(listView->viewport()->rect())) { listView->itemDelegate(index)->paint(painter, option, index); } } } void DCategorizedView::Private::drawDraggedItems() { QRect rectToUpdate; QRect currentRect; foreach (const QModelIndex& index, listView->selectionModel()->selectedIndexes()) { int dx = mousePosition.x() - initialPressPosition.x() + listView->horizontalOffset(); int dy = mousePosition.y() - initialPressPosition.y() + listView->verticalOffset(); currentRect = visualRect(index); currentRect.adjust(dx, dy, dx, dy); if (currentRect.intersects(listView->viewport()->rect())) { rectToUpdate = rectToUpdate.united(currentRect); } } listView->viewport()->update(lastDraggedItemsRect.united(rectToUpdate)); lastDraggedItemsRect = rectToUpdate; } // ------------------------------------------------------------------------------------------------ DCategorizedView::DCategorizedView(QWidget* const parent) : QListView(parent), d(new Private(this)) { } DCategorizedView::~DCategorizedView() { delete d; } void DCategorizedView::setGridSize(const QSize& size) { QListView::setGridSize(size); slotLayoutChanged(); } void DCategorizedView::setModel(QAbstractItemModel* model) { d->lastSelection = QItemSelection(); d->forcedSelectionPosition = 0; d->hovered = QModelIndex(); d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; d->elementsInfo.clear(); d->elementsPosition.clear(); d->categoriesIndexes.clear(); d->categoriesPosition.clear(); d->categories.clear(); d->intersectedIndexes.clear(); if (d->proxyModel) { QObject::disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); QObject::disconnect(d->proxyModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(rowsRemoved(QModelIndex,int,int))); } QListView::setModel(model); d->proxyModel = dynamic_cast(model); if (d->proxyModel) { QObject::connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); QObject::connect(d->proxyModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(rowsRemoved(QModelIndex,int,int))); if (d->proxyModel->rowCount()) { slotLayoutChanged(); } } } QRect DCategorizedView::visualRect(const QModelIndex& index) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QListView::visualRect(index); } if (!qobject_cast(index.model())) { return d->visualRect(d->proxyModel->mapFromSource(index)); } return d->visualRect(index); } QRect DCategorizedView::categoryVisualRect(const QModelIndex& index) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QRect(); } if (!index.isValid()) { return QRect(); } QString category = d->elementsInfo[index.row()].category; return d->categoryVisualRect(category); } QModelIndex DCategorizedView::categoryAt(const QPoint& point) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QModelIndex(); } // We traverse the categories and find the first where point.y() is below the visualRect int y = 0, lastY = 0; QString lastCategory; foreach (const QString& category, d->categories) { y = d->categoryVisualRect(category).top(); - if ((point.y() >= lastY) && point.y() < y) + if ((point.y() >= lastY) && (point.y() < y)) { break; } lastY = y; y = 0; lastCategory = category; } // if lastCategory is the last one in the list y will be 0 if (!lastCategory.isNull() && (point.y() >= lastY) && ((point.y() < y) || !y)) { return d->proxyModel->index(d->categoriesIndexes[lastCategory][0], d->proxyModel->sortColumn()); } return QModelIndex(); } QItemSelectionRange DCategorizedView::categoryRange(const QModelIndex& index) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QItemSelectionRange(); } if (!index.isValid()) { return QItemSelectionRange(); } QString category = d->elementsInfo[index.row()].category; QModelIndex first = d->proxyModel->index(d->categoriesIndexes[category].first(), d->proxyModel->sortColumn()); QModelIndex last = d->proxyModel->index(d->categoriesIndexes[category].last(), d->proxyModel->sortColumn()); + return QItemSelectionRange(first, last); } DCategoryDrawer* DCategorizedView::categoryDrawer() const { return d->categoryDrawer; } void DCategorizedView::setCategoryDrawer(DCategoryDrawer* categoryDrawer) { d->lastSelection = QItemSelection(); d->forcedSelectionPosition = 0; d->hovered = QModelIndex(); d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; d->elementsInfo.clear(); d->elementsPosition.clear(); d->categoriesIndexes.clear(); d->categoriesPosition.clear(); d->categories.clear(); d->intersectedIndexes.clear(); d->categoryDrawer = categoryDrawer; if (categoryDrawer) { if (d->proxyModel) { if (d->proxyModel->rowCount()) { slotLayoutChanged(); } } } else { updateGeometries(); } } void DCategorizedView::setDrawDraggedItems(bool drawDraggedItems) { d->drawItemsWhileDragging = drawDraggedItems; } QModelIndex DCategorizedView::indexAt(const QPoint& point) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QListView::indexAt(point); } QModelIndex index; const QModelIndexList item = d->intersectionSet(QRect(point, point)); if (item.count() == 1) { index = item[0]; } return index; } QModelIndexList DCategorizedView::categorizedIndexesIn(const QRect& rect) const { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return QModelIndexList(); } return d->intersectionSet(rect); } void DCategorizedView::reset() { QListView::reset(); d->lastSelection = QItemSelection(); d->forcedSelectionPosition = 0; d->hovered = QModelIndex(); d->biggestItemSize = QSize(0, 0); d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; d->elementsInfo.clear(); d->elementsPosition.clear(); d->categoriesIndexes.clear(); d->categoriesPosition.clear(); d->categories.clear(); d->intersectedIndexes.clear(); } void DCategorizedView::paintEvent(QPaintEvent* event) { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { QListView::paintEvent(event); return; } bool alternatingRows = alternatingRowColors(); QStyleOptionViewItem option = viewOptions(); option.widget = this; if (wordWrap()) { option.features |= QStyleOptionViewItem::WrapText; } QPainter painter(viewport()); QRect area = event->rect(); const bool focus = (hasFocus() || viewport()->hasFocus()) && currentIndex().isValid(); const QStyle::State state = option.state; const bool enabled = (state & QStyle::State_Enabled) != 0; painter.save(); QModelIndexList dirtyIndexes = d->intersectionSet(area); bool alternate = false; if (dirtyIndexes.count()) { alternate = dirtyIndexes[0].row() % 2; } foreach (const QModelIndex& index, dirtyIndexes) { - if (alternatingRows && alternate) + if (alternatingRows && alternate) { option.features |= QStyleOptionViewItem::Alternate; alternate = false; } else if (alternatingRows) { option.features &= ~QStyleOptionViewItem::Alternate; alternate = true; } option.state = state; option.rect = visualRect(index); if (selectionModel() && selectionModel()->isSelected(index)) { option.state |= QStyle::State_Selected; } if (enabled) { QPalette::ColorGroup cg; if ((d->proxyModel->flags(index) & Qt::ItemIsEnabled) == 0) { option.state &= ~QStyle::State_Enabled; - cg = QPalette::Disabled; + cg = QPalette::Disabled; } else { cg = QPalette::Normal; } option.palette.setCurrentColorGroup(cg); } if (focus && (currentIndex() == index)) { option.state |= QStyle::State_HasFocus; if (this->state() == EditingState) { option.state |= QStyle::State_Editing; } } if (index == d->hovered) { option.state |= QStyle::State_MouseOver; } else { option.state &= ~QStyle::State_MouseOver; } itemDelegate(index)->paint(&painter, option, index); } // Redraw categories QStyleOptionViewItem otherOption; bool intersectedInThePast = false; foreach (const QString& category, d->categories) { otherOption = option; otherOption.rect = d->categoryVisualRect(category); otherOption.state &= ~QStyle::State_MouseOver; - if (otherOption.rect.intersects(area)) + if (otherOption.rect.intersects(area)) { intersectedInThePast = true; QModelIndex indexToDraw = d->proxyModel->index(d->categoriesIndexes[category][0], d->proxyModel->sortColumn()); d->drawNewCategory(indexToDraw, d->proxyModel->sortRole(), otherOption, &painter); } else if (intersectedInThePast) { // the visible area has been finished, we don't need to keep asking, the rest won't intersect // this is doable because we know that categories are correctly ordered on the list. break; } } if ((selectionMode() != SingleSelection) && (selectionMode() != NoSelection)) { - if (d->mouseButtonPressed && QListView::state() != DraggingState) + if (d->mouseButtonPressed && (QListView::state() != DraggingState)) { QPoint start, end, initialPressPosition; initialPressPosition = d->initialPressPosition; initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); if ((d->initialPressPosition.x() > d->mousePosition.x()) || (d->initialPressPosition.y() > d->mousePosition.y())) { start = d->mousePosition; end = initialPressPosition; } else { start = initialPressPosition; end = d->mousePosition; } QStyleOptionRubberBand yetAnotherOption; yetAnotherOption.initFrom(this); yetAnotherOption.shape = QRubberBand::Rectangle; yetAnotherOption.opaque = false; yetAnotherOption.rect = QRect(start, end).intersected(viewport()->rect().adjusted(-16, -16, 16, 16)); painter.save(); style()->drawControl(QStyle::CE_RubberBand, &yetAnotherOption, &painter); painter.restore(); } } - if (d->drawItemsWhileDragging && QListView::state() == DraggingState && !d->dragLeftViewport) + if (d->drawItemsWhileDragging && (QListView::state() == DraggingState) && !d->dragLeftViewport) { painter.setOpacity(0.5); d->drawDraggedItems(&painter); } painter.restore(); } void DCategorizedView::resizeEvent(QResizeEvent* event) { QListView::resizeEvent(event); // Clear the items positions cache d->elementsPosition.clear(); d->categoriesPosition.clear(); d->forcedSelectionPosition = 0; if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return; } d->updateScrollbars(); } QItemSelection DCategorizedView::Private::selectionForRect(const QRect& rect) { QItemSelection selection; QModelIndex tl, br; QModelIndexList intersectedIndexes = intersectionSet(rect); QList::const_iterator it = intersectedIndexes.constBegin(); for ( ; it != intersectedIndexes.constEnd() ; ++it) { if (!tl.isValid() && !br.isValid()) { tl = br = *it; } else if ((*it).row() == (tl.row() - 1)) { tl = *it; // expand current range } else if ((*it).row() == (br.row() + 1)) { br = (*it); // expand current range } else { selection.select(tl, br); // select current range tl = br = *it; // start new range } } if (tl.isValid() && br.isValid()) { selection.select(tl, br); } else if (tl.isValid()) { selection.select(tl, tl); } else if (br.isValid()) { selection.select(br, br); } return selection; } void DCategorizedView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command) { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { QListView::setSelection(rect, command); return; } QItemSelection selection; /* QRect contentsRect = rect.translated(horizontalOffset(), verticalOffset()); */ QModelIndexList intersectedIndexes = d->intersectionSet(rect); if ((rect.width() == 1) && (rect.height() == 1)) { QModelIndex tl; if (!intersectedIndexes.isEmpty()) { tl = intersectedIndexes.last(); // special case for mouse press; only select the top item } if (tl.isValid() && (tl.flags() & Qt::ItemIsEnabled)) { selection.select(tl, tl); } } else { if (state() == DragSelectingState) { // visual selection mode (rubberband selection) selection = d->selectionForRect(rect); } else { // logical selection mode (key and mouse click selection) QModelIndex tl, br; // get the first item const QRect topLeft(rect.left(), rect.top(), 1, 1); intersectedIndexes = d->intersectionSet(topLeft); if (!intersectedIndexes.isEmpty()) { tl = intersectedIndexes.last(); } // get the last item const QRect bottomRight(rect.right(), rect.bottom(), 1, 1); intersectedIndexes = d->intersectionSet(bottomRight); if (!intersectedIndexes.isEmpty()) { br = intersectedIndexes.last(); } // get the ranges if (tl.isValid() && br.isValid() && (tl.flags() & Qt::ItemIsEnabled) && (br.flags() & Qt::ItemIsEnabled)) { // first, middle, last in content coordinates QRect middle; QRect first = d->cachedRectIndex(tl); QRect last = d->cachedRectIndex(br); QSize fullSize = d->contentsSize(); if (flow() == LeftToRight) { QRect& top = first; QRect& bottom = last; // if bottom is above top, swap them if (top.center().y() > bottom.center().y()) { QRect tmp = top; top = bottom; bottom = tmp; } // if the rect are on differnet lines, expand if (top.top() != bottom.top()) { // top rectangle if (isRightToLeft()) { top.setLeft(0); } else { top.setRight(fullSize.width()); } // bottom rectangle if (isRightToLeft()) { bottom.setRight(fullSize.width()); } else { bottom.setLeft(0); } } else if (top.left() > bottom.right()) { if (isRightToLeft()) { bottom.setLeft(top.right()); } else { bottom.setRight(top.left()); } } else { if (isRightToLeft()) { top.setLeft(bottom.right()); } else { top.setRight(bottom.left()); } } // middle rectangle if (top.bottom() < bottom.top()) { middle.setTop(top.bottom() + 1); middle.setLeft(qMin(top.left(), bottom.left())); middle.setBottom(bottom.top() - 1); middle.setRight(qMax(top.right(), bottom.right())); } } else { // TopToBottom QRect& left = first; QRect& right = last; if (left.center().x() > right.center().x()) { std::swap(left, right); } int ch = fullSize.height(); if (left.left() != right.left()) { // left rectangle if (isRightToLeft()) { left.setTop(0); } else { left.setBottom(ch); } // top rectangle if (isRightToLeft()) { right.setBottom(ch); } else { right.setTop(0); } // only set middle if the middle.setTop(0); middle.setBottom(ch); middle.setLeft(left.right() + 1); middle.setRight(right.left() - 1); } else if (left.bottom() < right.top()) { left.setBottom(right.top() - 1); } else { right.setBottom(left.top() - 1); } } // get viewport coordinates first = first.translated( - horizontalOffset(), - verticalOffset()); middle = middle.translated( - horizontalOffset(), - verticalOffset()); last = last.translated( - horizontalOffset(), - verticalOffset()); // do the selections QItemSelection topSelection = d->selectionForRect(first); QItemSelection middleSelection = d->selectionForRect(middle); QItemSelection bottomSelection = d->selectionForRect(last); // merge selection.merge(topSelection, QItemSelectionModel::Select); selection.merge(middleSelection, QItemSelectionModel::Select); selection.merge(bottomSelection, QItemSelectionModel::Select); } } } selectionModel()->select(selection, command); } void DCategorizedView::mouseMoveEvent(QMouseEvent* event) { QListView::mouseMoveEvent(event); // was a dragging started? if (state() == DraggingState) { d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; if (d->drawItemsWhileDragging) { viewport()->update(d->lastDraggedItemsRect); } } if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return; } const QModelIndexList item = d->intersectionSet(QRect(event->pos(), event->pos())); if (item.count() == 1) { d->hovered = item[0]; } else { d->hovered = QModelIndex(); } const QString previousHoveredCategory = d->hoveredCategory; d->mousePosition = event->pos(); d->hoveredCategory.clear(); // Redraw categories foreach (const QString& category, d->categories) { - if (d->categoryVisualRect(category).intersects(QRect(event->pos(), event->pos()))) + if (d->categoryVisualRect(category).intersects(QRect(event->pos(), event->pos()))) { d->hoveredCategory = category; viewport()->update(d->categoryVisualRect(category)); } else if ((category == previousHoveredCategory) && (!d->categoryVisualRect(previousHoveredCategory).intersects(QRect(event->pos(), event->pos())))) { viewport()->update(d->categoryVisualRect(category)); } } QRect rect; if (d->mouseButtonPressed && QListView::state() != DraggingState) { QPoint start, end, initialPressPosition; initialPressPosition = d->initialPressPosition; initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); if (d->initialPressPosition.x() > d->mousePosition.x() || d->initialPressPosition.y() > d->mousePosition.y()) { start = d->mousePosition; end = initialPressPosition; } else { start = initialPressPosition; end = d->mousePosition; } rect = QRect(start, end).adjusted(-16, -16, 16, 16); rect = rect.united(QRect(start, end).adjusted(16, 16, -16, -16)).intersected(viewport()->rect()); viewport()->update(rect); } } void DCategorizedView::mousePressEvent(QMouseEvent* event) { d->dragLeftViewport = false; QListView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { d->mouseButtonPressed = true; d->initialPressPosition = event->pos(); d->initialPressPosition.setY(d->initialPressPosition.y() + verticalOffset()); d->initialPressPosition.setX(d->initialPressPosition.x() + horizontalOffset()); } else if (event->button() == Qt::RightButton) { d->rightMouseButtonPressed = true; } if (selectionModel()) { d->lastSelection = selectionModel()->selection(); } viewport()->update(d->categoryVisualRect(d->hoveredCategory)); } void DCategorizedView::mouseReleaseEvent(QMouseEvent* event) { d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; QListView::mouseReleaseEvent(event); if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return; } - QPoint initialPressPosition = viewport()->mapFromGlobal(QCursor::pos()); - initialPressPosition.setY(initialPressPosition.y() + verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() + horizontalOffset()); + QPoint initPressPos = viewport()->mapFromGlobal(QCursor::pos()); + initPressPos.setY(initPressPos.y() + verticalOffset()); + initPressPos.setX(initPressPos.x() + horizontalOffset()); if ((selectionMode() != SingleSelection) && (selectionMode() != NoSelection) && - (initialPressPosition == d->initialPressPosition)) + (initPressPos == d->initialPressPosition)) { foreach (const QString& category, d->categories) { if (d->categoryVisualRect(category).contains(event->pos()) && selectionModel()) { QItemSelection selection = selectionModel()->selection(); const QVector &indexList = d->categoriesIndexes[category]; foreach (int row, indexList) { QModelIndex selectIndex = d->proxyModel->index(row, 0); selection << QItemSelectionRange(selectIndex); } selectionModel()->select(selection, QItemSelectionModel::SelectCurrent); break; } } } QRect rect; if (state() != DraggingState) { - QPoint start, end, initialPressPosition; + QPoint start, end, newInitPressPos; - initialPressPosition = d->initialPressPosition; + newInitPressPos = d->initialPressPosition; - initialPressPosition.setY(initialPressPosition.y() - verticalOffset()); - initialPressPosition.setX(initialPressPosition.x() - horizontalOffset()); + newInitPressPos.setY(newInitPressPos.y() - verticalOffset()); + newInitPressPos.setX(newInitPressPos.x() - horizontalOffset()); if ((d->initialPressPosition.x() > d->mousePosition.x()) || (d->initialPressPosition.y() > d->mousePosition.y())) { start = d->mousePosition; - end = initialPressPosition; + end = newInitPressPos; } else { - start = initialPressPosition; + start = newInitPressPos; end = d->mousePosition; } rect = QRect(start, end).adjusted(-16, -16, 16, 16); rect = rect.united(QRect(start, end).adjusted(16, 16, -16, -16)).intersected(viewport()->rect()); viewport()->update(rect); } if (d->hovered.isValid()) { viewport()->update(visualRect(d->hovered)); } else if (!d->hoveredCategory.isEmpty()) { viewport()->update(d->categoryVisualRect(d->hoveredCategory)); } } void DCategorizedView::leaveEvent(QEvent* event) { d->hovered = QModelIndex(); d->hoveredCategory.clear(); QListView::leaveEvent(event); } void DCategorizedView::startDrag(Qt::DropActions supportedActions) { // FIXME: QAbstractItemView does far better here since it sets the // pixmap of selected icons to the dragging cursor, but it sets a non // ARGB window so it is no transparent. Use QAbstractItemView when // this is fixed on Qt. // QAbstractItemView::startDrag(supportedActions); #if defined(DOLPHIN_DRAGANDDROP) + Q_UNUSED(supportedActions); + #else + QListView::startDrag(supportedActions); + #endif } void DCategorizedView::dragMoveEvent(QDragMoveEvent* event) { d->mousePosition = event->pos(); d->dragLeftViewport = false; #if defined(DOLPHIN_DRAGANDDROP) + QAbstractItemView::dragMoveEvent(event); + #else + QListView::dragMoveEvent(event); + #endif if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { return; } d->hovered = indexAt(event->pos()); #if !defined(DOLPHIN_DRAGANDDROP) + d->drawDraggedItems(); + #endif } void DCategorizedView::dragLeaveEvent(QDragLeaveEvent* event) { d->dragLeftViewport = true; #if defined(DOLPHIN_DRAGANDDROP) + QAbstractItemView::dragLeaveEvent(event); + #else + QListView::dragLeaveEvent(event); + #endif } void DCategorizedView::dropEvent(QDropEvent* event) { #if defined(DOLPHIN_DRAGANDDROP) + QAbstractItemView::dropEvent(event); + #else + QListView::dropEvent(event); + #endif } QModelIndex DCategorizedView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { if ((viewMode() != DCategorizedView::IconMode) || !d->proxyModel || !d->categoryDrawer || d->categories.isEmpty() || !d->proxyModel->isCategorizedModel()) { return QListView::moveCursor(cursorAction, modifiers); } int viewportWidth = viewport()->width() - spacing(); int itemWidth; if (gridSize().isEmpty()) { itemWidth = d->biggestItemSize.width(); } else { itemWidth = gridSize().width(); } int itemWidthPlusSeparation = spacing() + itemWidth; if (!itemWidthPlusSeparation) { ++itemWidthPlusSeparation; } int elementsPerRow = viewportWidth / itemWidthPlusSeparation; if (!elementsPerRow) { ++elementsPerRow; } QModelIndex current = selectionModel() ? selectionModel()->currentIndex() : QModelIndex(); if (!current.isValid()) { if (cursorAction == MoveEnd) { current = model()->index(model()->rowCount() - 1, 0, QModelIndex()); /* d->forcedSelectionPosition = d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow; */ } else { current = model()->index(0, 0, QModelIndex()); d->forcedSelectionPosition = 0; } return current; } QString lastCategory = d->categories.first(); QString theCategory = d->categories.first(); QString afterCategory = d->categories.first(); bool hasToBreak = false; foreach (const QString& category, d->categories) { if (hasToBreak) { afterCategory = category; break; } if (category == d->elementsInfo[current.row()].category) { theCategory = category; hasToBreak = true; } if (!hasToBreak) { lastCategory = category; } } switch (cursorAction) { case QAbstractItemView::MovePageUp: { // We need to reimplement PageUp/Down as well because // default QListView implementation will not work properly with our custom layout QModelIndexList visibleIndexes = d->intersectionSet(viewport()->rect()); if (!visibleIndexes.isEmpty()) { int indexToMove = qMax(current.row() - visibleIndexes.size(), 0); + return d->proxyModel->index(indexToMove, 0); } break; } // fall through case QAbstractItemView::MoveUp: { if (d->elementsInfo[current.row()].relativeOffsetToCategory >= elementsPerRow) { int indexToMove = current.row(); indexToMove -= qMin(((d->elementsInfo[current.row()].relativeOffsetToCategory) + d->forcedSelectionPosition), elementsPerRow - d->forcedSelectionPosition + (d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)); return (d->proxyModel->index(indexToMove, 0)); } else { int lastCategoryLastRow = (d->categoriesIndexes[lastCategory].count() - 1) % elementsPerRow; int indexToMove = current.row() - d->elementsInfo[current.row()].relativeOffsetToCategory; if (d->forcedSelectionPosition >= lastCategoryLastRow) { indexToMove -= 1; } else { indexToMove -= qMin((lastCategoryLastRow - d->forcedSelectionPosition + 1), d->forcedSelectionPosition + elementsPerRow + 1); } return d->proxyModel->index(indexToMove, 0); } } case QAbstractItemView::MovePageDown: { QModelIndexList visibleIndexes = d->intersectionSet(viewport()->rect()); if (!visibleIndexes.isEmpty()) { int indexToMove = qMin(current.row() + visibleIndexes.size(), d->elementsInfo.size() - 1); + return d->proxyModel->index(indexToMove, 0); } } // fall through case QAbstractItemView::MoveDown: { if (d->elementsInfo[current.row()].relativeOffsetToCategory < (d->categoriesIndexes[theCategory].count() - 1 - ((d->categoriesIndexes[theCategory].count() - 1) % elementsPerRow))) { int indexToMove = current.row(); indexToMove += qMin(elementsPerRow, d->categoriesIndexes[theCategory].count() - 1 - d->elementsInfo[current.row()].relativeOffsetToCategory); return d->proxyModel->index(indexToMove, 0); } else { int afterCategoryLastRow = qMin(elementsPerRow, d->categoriesIndexes[afterCategory].count()); int indexToMove = current.row() + (d->categoriesIndexes[theCategory].count() - d->elementsInfo[current.row()].relativeOffsetToCategory); if (d->forcedSelectionPosition >= afterCategoryLastRow) { indexToMove += afterCategoryLastRow - 1; } else { indexToMove += qMin(d->forcedSelectionPosition, elementsPerRow); } return d->proxyModel->index(indexToMove, 0); } } case QAbstractItemView::MoveLeft: if (layoutDirection() == Qt::RightToLeft) { if (((current.row() + 1) == d->elementsInfo.size()) || !(d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow)) { return current; } return d->proxyModel->index(current.row() + 1, 0); } if ((current.row() == 0) || !(d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)) { return current; } return d->proxyModel->index(current.row() - 1, 0); case QAbstractItemView::MoveRight: if (layoutDirection() == Qt::RightToLeft) { if ((current.row() == 0) || !(d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow)) { return current; } return d->proxyModel->index(current.row() - 1, 0); } if (((current.row() + 1) == d->elementsInfo.size()) || !(d->elementsInfo[current.row() + 1].relativeOffsetToCategory % elementsPerRow)) { return current; } return d->proxyModel->index(current.row() + 1, 0); default: break; } return QListView::moveCursor(cursorAction, modifiers); } void DCategorizedView::rowsInserted(const QModelIndex& parent, int start, int end) { QListView::rowsInserted(parent, start, end); if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { d->forcedSelectionPosition = 0; d->hovered = QModelIndex(); d->biggestItemSize = QSize(0, 0); d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; d->elementsInfo.clear(); d->elementsPosition.clear(); d->categoriesIndexes.clear(); d->categoriesPosition.clear(); d->categories.clear(); d->intersectedIndexes.clear(); return; } rowsInsertedArtifficial(parent, start, end); } int DCategorizedView::Private::categoryUpperBound(SparseModelIndexVector& modelIndexList, int begin, int averageSize) { int end = modelIndexList.size(); QString category = proxyModel->data(modelIndexList[begin], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); // First case: Small category with <10 entries const int smallEnd = qMin(end, begin + 10); for (int k = begin ; k < smallEnd ; ++k) { if (category != proxyModel->data(modelIndexList[k], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()) { return k; } } begin += 10; // Second case: only one category, test last value QString value = proxyModel->data(modelIndexList[end - 1], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); if (value == category) { return end; } // Third case: use average of last category sizes if (averageSize && ((begin + averageSize) < end)) { if (category != proxyModel->data(modelIndexList[begin + averageSize], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()) { end = begin + averageSize; } else if (begin + 2*averageSize < end) { if (category != proxyModel->data(modelIndexList[begin + 2*averageSize], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()) { end = begin + 2 * averageSize; } } } // now apply a binary search - the model is sorted by category // from qUpperBound, Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) int middle; int n = end - begin; int half; while (n > 0) { half = n >> 1; middle = begin + half; if (category != proxyModel->data(modelIndexList[middle], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()) { n = half; } else { begin = middle + 1; n -= half + 1; } } return begin; } void DCategorizedView::rowsInsertedArtifficial(const QModelIndex& parent, int start, int end) { Q_UNUSED(parent); d->forcedSelectionPosition = 0; d->hovered = QModelIndex(); d->biggestItemSize = QSize(0, 0); d->mouseButtonPressed = false; d->rightMouseButtonPressed = false; d->elementsInfo.clear(); d->elementsPosition.clear(); d->categoriesIndexes.clear(); d->categoriesPosition.clear(); d->categories.clear(); d->intersectedIndexes.clear(); if ((start > end) || (end < 0) || (start < 0) || !d->proxyModel->rowCount()) { return; } // Add all elements mapped to the source model and explore categories const int rowCount = d->proxyModel->rowCount(); const int sortColumn = d->proxyModel->sortColumn(); QString lastCategory = d->proxyModel->data(d->proxyModel->index(0, sortColumn), DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); int offset = -1; SparseModelIndexVector modelIndexList(rowCount, d->proxyModel, sortColumn); d->elementsInfo = QVector(rowCount); int categorySizes = 0; int categoryCounts = 0; if (uniformItemSizes()) { // use last index as sample for size hint QModelIndex sample = d->proxyModel->index(rowCount - 1, modelColumn(), rootIndex()); d->biggestItemSize = sizeHintForIndex(sample); } else { QStyleOptionViewItem option = viewOptions(); for (int k = 0 ; k < rowCount ; ++k) { QModelIndex indexSize = (sortColumn == 0) ? modelIndexList[k] : d->proxyModel->index(k, 0); QSize hint = itemDelegate(indexSize)->sizeHint(option, indexSize); d->biggestItemSize = QSize(qMax(hint.width(), d->biggestItemSize.width()), qMax(hint.height(), d->biggestItemSize.height())); } } for (int k = 0 ; k < rowCount ; ) { lastCategory = d->proxyModel->data(modelIndexList[k], DCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); int upperBound = d->categoryUpperBound(modelIndexList, k, categorySizes / ++categoryCounts); categorySizes += upperBound - k; offset = 0; QVector rows(upperBound - k); for (int i = k ; i < upperBound ; ++i, ++offset) { rows[offset] = i; struct Private::ElementInfo& elementInfo = d->elementsInfo[i]; elementInfo.category = lastCategory; elementInfo.relativeOffsetToCategory = offset; } k = upperBound; d->categoriesIndexes.insert(lastCategory, rows); d->categories << lastCategory; } d->updateScrollbars(); // FIXME: We need to safely save the last selection. This is on my TODO // list (ereslibre). // Note: QItemSelectionModel will save it selection in persistend indexes // on layoutChanged(). All works fine for me. //selectionModel()->clear(); } void DCategorizedView::rowsRemoved(const QModelIndex& parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); if (d->proxyModel && d->categoryDrawer && d->proxyModel->isCategorizedModel()) { // Force the view to update all elements rowsInsertedArtifficial(QModelIndex(), 0, d->proxyModel->rowCount() - 1); } } void DCategorizedView::updateGeometries() { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { QListView::updateGeometries(); return; } // Avoid QListView::updateGeometries(), since it will try to set another - // range to our scroll bars, what we don't want (ereslibre) + // range to our scroll bars, what we don't want + QAbstractItemView::updateGeometries(); } void DCategorizedView::slotLayoutChanged() { if (d->proxyModel && d->categoryDrawer && d->proxyModel->isCategorizedModel()) { // all cached values are invalidated, recompute immediately rowsInsertedArtifficial(QModelIndex(), 0, d->proxyModel->rowCount() - 1); } } void DCategorizedView::currentChanged(const QModelIndex& current, const QModelIndex& previous) { if (!d->proxyModel || !d->categoryDrawer || !d->proxyModel->isCategorizedModel()) { QListView::currentChanged(current, previous); return; } // We need to update the forcedSelectionPosition property in order to correctly // navigate after with keyboard using up & down keys int viewportWidth = viewport()->width() - spacing(); // int itemHeight; int itemWidth; if (gridSize().isEmpty()) { // itemHeight = d->biggestItemSize.height(); itemWidth = d->biggestItemSize.width(); } else { // itemHeight = gridSize().height(); itemWidth = gridSize().width(); } int itemWidthPlusSeparation = spacing() + itemWidth; if (!itemWidthPlusSeparation) { ++itemWidthPlusSeparation; } int elementsPerRow = viewportWidth / itemWidthPlusSeparation; if (!elementsPerRow) { ++elementsPerRow; } if (current.isValid()) { d->forcedSelectionPosition = d->elementsInfo[current.row()].relativeOffsetToCategory % elementsPerRow; } QListView::currentChanged(current, previous); } } // namespace Digikam diff --git a/core/libs/widgets/itemview/dcategorizedview_p.h b/core/libs/widgets/itemview/dcategorizedview_p.h index e4ba7cfbef..56a2f6f49b 100644 --- a/core/libs/widgets/itemview/dcategorizedview_p.h +++ b/core/libs/widgets/itemview/dcategorizedview_p.h @@ -1,234 +1,230 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2010-01-16 * Description : Item view for listing items in a categorized fashion optionally * * Copyright (C) 2007 by Rafael Fernández López * Copyright (C) 2009-2012 by Marcel Wiesweg * Copyright (C) 2011-2020 by Gilles Caulier * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_DCATEGORIZED_VIEW_PRIVATE_H #define DIGIKAM_DCATEGORIZED_VIEW_PRIVATE_H #include "dcategorizedview.h" // Qt includes #include class DCategoryDrawer; namespace Digikam { class DCategorizedSortFilterProxyModel; class Q_DECL_HIDDEN SparseModelIndexVector : public QVector { public: explicit SparseModelIndexVector(int rowCount, QAbstractItemModel* const model_, int column_) : QVector(rowCount), model(model_), column(column_) { } inline QModelIndex& operator[](int i) { QModelIndex& index = QVector::operator[](i); if (!index.isValid()) { index = model->index(i, column); } return index; } private: // not to be used const QModelIndex& operator[](int i) const { return QVector::operator[](i); } private: QAbstractItemModel* model; int column; }; // ------------------------------------------------------------------------------------ class Q_DECL_HIDDEN DCategorizedView::Private { public: explicit Private(DCategorizedView* const listView); ~Private(); // Methods /** * Returns the list of items that intersects with @p rect */ const QModelIndexList& intersectionSet(const QRect& rect); /** * Gets the item rect in the viewport for @p index */ QRect visualRectInViewport(const QModelIndex& index) const; /** * Returns the category rect in the viewport for @p category */ QRect visualCategoryRectInViewport(const QString& category) const; /** * Caches and returns the rect that corresponds to @p index */ const QRect& cacheIndex(const QModelIndex& index); /** * Caches and returns the rect that corresponds to @p category */ const QRect& cacheCategory(const QString& category); /** * Returns the rect that corresponds to @p index * @note If the rect is not cached, it becomes cached */ const QRect& cachedRectIndex(const QModelIndex& index); /** * Returns the rect that corresponds to @p category * @note If the rect is not cached, it becomes cached */ const QRect& cachedRectCategory(const QString& category); /** * Returns the visual rect (taking in count x and y offsets) for @p index * @note If the rect is not cached, it becomes cached */ QRect visualRect(const QModelIndex& index); /** * Returns the visual rect (taking in count x and y offsets) for @p category * @note If the rect is not cached, it becomes cached */ QRect categoryVisualRect(const QString& category); /** * Returns the contents size of this view (topmost category to bottommost index + spacing) */ QSize contentsSize(); /** * This method will draw a new category represented by index * @p index on the rect specified by @p option.rect, with * painter @p painter */ void drawNewCategory(const QModelIndex& index, int sortRole, const QStyleOption& option, QPainter* painter); /** * This method will update scrollbars ranges. Called when our model changes * or when the view is resized */ void updateScrollbars(); /** * This method will draw dragged items in the painting operation */ void drawDraggedItems(QPainter* painter); /** * This method will determine which rect needs to be updated because of a * dragging operation */ void drawDraggedItems(); /** * This method will, starting from the index at begin in the given (sorted) modelIndex List, * find the last index having the same category as the index to begin with. */ int categoryUpperBound(SparseModelIndexVector& modelIndexList, int begin, int averageSize = 0); /** * Returns a QItemSelection for all items intersection rect. */ QItemSelection selectionForRect(const QRect& rect); public: /// Attributes - struct ElementInfo { QString category; int relativeOffsetToCategory; }; public: /// Basic data - DCategorizedView* listView; DCategoryDrawer* categoryDrawer; QSize biggestItemSize; /// Behavior data - bool mouseButtonPressed; bool rightMouseButtonPressed; bool dragLeftViewport; bool drawItemsWhileDragging; QModelIndex hovered; QString hoveredCategory; QPoint initialPressPosition; QPoint mousePosition; int forcedSelectionPosition; /** * Cache data * We cannot merge some of them into structs because it would affect * performance */ QVector elementsInfo; QHash elementsPosition; QHash > categoriesIndexes; QHash categoriesPosition; QStringList categories; QModelIndexList intersectedIndexes; QRect lastDraggedItemsRect; QItemSelection lastSelection; /// Attributes for speed reasons - DCategorizedSortFilterProxyModel* proxyModel; }; } // namespace Digikam #endif // DIGIKAM_DCATEGORIZED_VIEW_PRIVATE_H diff --git a/core/libs/widgets/layout/sidebar.cpp b/core/libs/widgets/layout/sidebar.cpp index aeb4caeb85..2f5ce23d7f 100644 --- a/core/libs/widgets/layout/sidebar.cpp +++ b/core/libs/widgets/layout/sidebar.cpp @@ -1,1473 +1,1475 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2005-03-22 * Description : a widget to manage sidebar in GUI. * * Copyright (C) 2005-2006 by Joern Ahrens * Copyright (C) 2006-2020 by Gilles Caulier * Copyright (C) 2008-2011 by Marcel Wiesweg * Copyright (C) 2001-2003 by Joseph Wenninger * * 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, 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. * * ============================================================ */ #include "sidebar.h" // C++ includes #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "digikam_debug.h" namespace Digikam { class Q_DECL_HIDDEN DMultiTabBarFrame::Private { public: explicit Private() : mainLayout(nullptr), position(Qt::LeftEdge), style(DMultiTabBar::AllIconsText) { } QBoxLayout* mainLayout; QList tabs; Qt::Edge position; DMultiTabBar::TextStyle style; }; DMultiTabBarFrame::DMultiTabBarFrame(QWidget* const parent, Qt::Edge pos) : QFrame(parent), d(new Private) { d->position = pos; if ((pos == Qt::LeftEdge) || (pos == Qt::RightEdge)) { d->mainLayout = new QVBoxLayout(this); } else { d->mainLayout = new QHBoxLayout(this); } d->mainLayout->setContentsMargins(QMargins()); d->mainLayout->setSpacing(0); d->mainLayout->addStretch(); setFrameStyle(NoFrame); setBackgroundRole(QPalette::Window); } DMultiTabBarFrame::~DMultiTabBarFrame() { qDeleteAll(d->tabs); d->tabs.clear(); delete d; } void DMultiTabBarFrame::setStyle(DMultiTabBar::TextStyle style) { d->style = style; for (int i = 0 ; i < d->tabs.count() ; ++i) { d->tabs.at(i)->setStyle(d->style); } updateGeometry(); } void DMultiTabBarFrame::contentsMousePressEvent(QMouseEvent* e) { e->ignore(); } void DMultiTabBarFrame::mousePressEvent(QMouseEvent* e) { e->ignore(); } DMultiTabBarTab* DMultiTabBarFrame::tab(int id) const { QListIterator it(d->tabs); while (it.hasNext()) { DMultiTabBarTab* const tab = it.next(); if (tab->id() == id) { return tab; } } return nullptr; } int DMultiTabBarFrame::appendTab(const QPixmap& pic, int id, const QString& text) { DMultiTabBarTab* const tab = new DMultiTabBarTab(pic, text, id, this, d->position, d->style); d->tabs.append(tab); // Insert before the stretch. + d->mainLayout->insertWidget(d->tabs.size()-1, tab); tab->show(); return 0; } void DMultiTabBarFrame::removeTab(int id) { for (int pos = 0 ; pos < d->tabs.count() ; ++pos) { if (d->tabs.at(pos)->id() == id) { // remove & delete the tab + delete d->tabs.takeAt(pos); break; } } } void DMultiTabBarFrame::setPosition(Qt::Edge pos) { d->position = pos; for (int i = 0 ; i < d->tabs.count() ; ++i) { d->tabs.at(i)->setPosition(d->position); } updateGeometry(); } QList* DMultiTabBarFrame::tabs() { return &d->tabs; } // ------------------------------------------------------------------------------------- DMultiTabBarButton::DMultiTabBarButton(const QPixmap& pic, const QString& text, int id, QWidget* const parent) : QPushButton(QIcon(pic), text, parent), m_id(id) { // --- NOTE: use dynamic binding as slotClicked() is a virtual method which can be re-implemented in derived classes. connect(this, &QPushButton::clicked, this, &DMultiTabBarButton::slotClicked); // we can't see the focus, so don't take focus. #45557 // If keyboard navigation is wanted, then only the bar should take focus, // and arrows could change the focused button; but generally, tabbars don't take focus anyway. setFocusPolicy(Qt::NoFocus); // See RB #128005 setAttribute(Qt::WA_LayoutUsesWidgetRect); } DMultiTabBarButton::~DMultiTabBarButton() { } void DMultiTabBarButton::setText(const QString& text) { QPushButton::setText(text); } void DMultiTabBarButton::slotClicked() { updateGeometry(); emit clicked(m_id); } int DMultiTabBarButton::id() const { return m_id; } void DMultiTabBarButton::hideEvent(QHideEvent* e) { QPushButton::hideEvent(e); DMultiTabBar* const tb = dynamic_cast(parentWidget()); if (tb) { tb->updateSeparator(); } } void DMultiTabBarButton::showEvent(QShowEvent* e) { QPushButton::showEvent(e); DMultiTabBar* const tb = dynamic_cast(parentWidget()); if (tb) { tb->updateSeparator(); } } void DMultiTabBarButton::paintEvent(QPaintEvent*) { QStyleOptionButton opt; opt.initFrom(this); opt.icon = icon(); opt.iconSize = iconSize(); // removes the QStyleOptionButton::HasMenu ButtonFeature opt.features = QStyleOptionButton::Flat; QPainter painter(this); style()->drawControl(QStyle::CE_PushButton, &opt, &painter, this); } // ------------------------------------------------------------------------------------- class Q_DECL_HIDDEN DMultiTabBarTab::Private { public: explicit Private() : position(Qt::LeftEdge), style(DMultiTabBar::AllIconsText) { } Qt::Edge position; DMultiTabBar::TextStyle style; }; DMultiTabBarTab::DMultiTabBarTab(const QPixmap& pic, const QString& text, int id, QWidget* const parent, Qt::Edge pos, DMultiTabBar::TextStyle style) : DMultiTabBarButton(pic, text, id, parent), d(new Private) { d->style = style; d->position = pos; setToolTip(text); setCheckable(true); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); // shrink down to icon only, but prefer to show text if it's there } DMultiTabBarTab::~DMultiTabBarTab() { delete d; } void DMultiTabBarTab::setPosition(Qt::Edge pos) { d->position = pos; updateGeometry(); } void DMultiTabBarTab::setStyle(DMultiTabBar::TextStyle style) { d->style = style; updateGeometry(); } QPixmap DMultiTabBarTab::iconPixmap() const { int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this); return icon().pixmap(iconSize); } void DMultiTabBarTab::initStyleOption(QStyleOptionToolButton* opt) const { opt->initFrom(this); // Setup icon if (!icon().isNull()) { opt->iconSize = iconPixmap().size(); opt->icon = icon(); } // Should we draw text? if (shouldDrawText()) { opt->text = text(); } if (underMouse()) { opt->state |= QStyle::State_AutoRaise | QStyle::State_MouseOver | QStyle::State_Raised; } if (isChecked()) { opt->state |= QStyle::State_Sunken | QStyle::State_On; } opt->font = font(); opt->toolButtonStyle = shouldDrawText() ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly; opt->subControls = QStyle::SC_ToolButton; } QSize DMultiTabBarTab::sizeHint() const { return computeSizeHint(shouldDrawText()); } QSize DMultiTabBarTab::minimumSizeHint() const { return computeSizeHint(false); } void DMultiTabBarTab::computeMargins(int* hMargin, int* vMargin) const { // Unfortunately, QStyle does not give us enough information to figure out // where to place things, so we try to reverse-engineer it QStyleOptionToolButton opt; initStyleOption(&opt); QPixmap iconPix = iconPixmap(); QSize trialSize = iconPix.size(); QSize expandSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, trialSize, this); *hMargin = (expandSize.width() - trialSize.width())/2; *vMargin = (expandSize.height() - trialSize.height())/2; } QSize DMultiTabBarTab::computeSizeHint(bool withText) const { // Compute as horizontal first, then flip around if need be. QStyleOptionToolButton opt; initStyleOption(&opt); int hMargin, vMargin; computeMargins(&hMargin, &vMargin); // Compute interior size, starting from pixmap.. QPixmap iconPix = iconPixmap(); QSize size = iconPix.size(); // Always include text height in computation, to avoid resizing the minor direction // when expanding text.. QSize textSize = fontMetrics().size(0, text()); size.setHeight(qMax(size.height(), textSize.height())); // Pick margins for major/minor direction, depending on orientation int majorMargin = isVertical() ? vMargin : hMargin; int minorMargin = isVertical() ? hMargin : vMargin; size.setWidth (size.width() + 2*majorMargin); size.setHeight(size.height() + 2*minorMargin); if (withText) { // Add enough room for the text, and an extra major margin. size.setWidth(size.width() + textSize.width() + majorMargin); } if (isVertical()) { return QSize(size.height(), size.width()); } return size; } void DMultiTabBarTab::setState(bool newState) { setChecked(newState); updateGeometry(); } void DMultiTabBarTab::setIcon(const QString& icon) { const QIcon i = QIcon::fromTheme(icon); const int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this); setIcon(i.pixmap(iconSize)); } void DMultiTabBarTab::setIcon(const QPixmap& icon) { QPushButton::setIcon(icon); } bool DMultiTabBarTab::shouldDrawText() const { return ((d->style == DMultiTabBar::AllIconsText) || isChecked()); } bool DMultiTabBarTab::isVertical() const { return ((d->position == Qt::RightEdge) || (d->position == Qt::LeftEdge)); } void DMultiTabBarTab::paintEvent(QPaintEvent*) { QPainter painter(this); QStyleOptionToolButton opt; initStyleOption(&opt); // Paint bevel.. if (underMouse() || isChecked()) { opt.text.clear(); opt.icon = QIcon(); style()->drawComplexControl(QStyle::CC_ToolButton, &opt, &painter, this); } int hMargin, vMargin; computeMargins(&hMargin, &vMargin); // We first figure out how much room we have for the text, based on // icon size and margin, try to fit in by eliding, and perhaps // give up on drawing the text entirely if we're too short on room QPixmap icon = iconPixmap(); int textRoom = 0; int iconRoom = 0; QString t; if (shouldDrawText()) { if (isVertical()) { iconRoom = icon.height() + 2*vMargin; textRoom = height() - iconRoom - vMargin; } else { iconRoom = icon.width() + 2*hMargin; textRoom = width() - iconRoom - hMargin; } t = painter.fontMetrics().elidedText(text(), Qt::ElideRight, textRoom); // See whether anything is left. Qt will return either // ... or the ellipsis unicode character, 0x2026 if ((t == QLatin1String("...")) || (t == QChar(0x2026))) { t.clear(); } } // Label time.... Simple case: no text, so just plop down the icon right in the center // We only do this when the button never draws the text, to avoid jumps in icon position // when resizing if (!shouldDrawText()) { style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter | Qt::AlignVCenter, icon); return; } // Now where the icon/text goes depends on text direction and tab position QRect iconArea; QRect labelArea; bool bottomIcon = false; bool rtl = layoutDirection() == Qt::RightToLeft; if (isVertical()) { if ((d->position == Qt::LeftEdge) && !rtl) { bottomIcon = true; } if ((d->position == Qt::RightEdge) && rtl) { bottomIcon = true; } } if (isVertical()) { if (bottomIcon) { labelArea = QRect(0, vMargin, width(), textRoom); iconArea = QRect(0, vMargin + textRoom, width(), iconRoom); } else { labelArea = QRect(0, iconRoom, width(), textRoom); iconArea = QRect(0, 0, width(), iconRoom); } } else { // Pretty simple --- depends only on RTL/LTR if (rtl) { labelArea = QRect(hMargin, 0, textRoom, height()); iconArea = QRect(hMargin + textRoom, 0, iconRoom, height()); } else { labelArea = QRect(iconRoom, 0, textRoom, height()); iconArea = QRect(0, 0, iconRoom, height()); } } style()->drawItemPixmap(&painter, iconArea, Qt::AlignCenter | Qt::AlignVCenter, icon); if (t.isEmpty()) { return; } QRect labelPaintArea = labelArea; if (isVertical()) { // If we're vertical, we paint to a simple 0,0 origin rect, // and get the transformations to get us in the right place labelPaintArea = QRect(0, 0, labelArea.height(), labelArea.width()); QTransform tr; if (bottomIcon) { tr.translate(labelArea.x(), labelPaintArea.width() + labelArea.y()); tr.rotate(-90); } else { tr.translate(labelPaintArea.height() + labelArea.x(), labelArea.y()); tr.rotate(90); } painter.setTransform(tr); } style()->drawItemText(&painter, labelPaintArea, Qt::AlignLeading | Qt::AlignVCenter, palette(), true, t, QPalette::ButtonText); } // ------------------------------------------------------------------------------------- class Q_DECL_HIDDEN DMultiTabBar::Private { public: explicit Private() : internal(nullptr), layout(nullptr), btnTabSep(nullptr), position(Qt::LeftEdge) { } DMultiTabBarFrame* internal; QBoxLayout* layout; QFrame* btnTabSep; QList buttons; Qt::Edge position; }; DMultiTabBar::DMultiTabBar(Qt::Edge pos, QWidget* const parent) : QWidget(parent), d(new Private) { if ((pos == Qt::LeftEdge) || (pos == Qt::RightEdge)) { d->layout = new QVBoxLayout(this); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); } else { d->layout = new QHBoxLayout(this); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } d->layout->setContentsMargins(QMargins()); d->layout->setSpacing(0); d->internal = new DMultiTabBarFrame(this, pos); setPosition(pos); setStyle(ActiveIconText); d->layout->insertWidget(0, d->internal); d->layout->insertWidget(0, d->btnTabSep = new QFrame(this)); d->btnTabSep->setFixedHeight(4); d->btnTabSep->setFrameStyle(QFrame::Panel | QFrame::Sunken); d->btnTabSep->setLineWidth(2); d->btnTabSep->hide(); updateGeometry(); } DMultiTabBar::~DMultiTabBar() { qDeleteAll(d->buttons); d->buttons.clear(); delete d; } int DMultiTabBar::appendButton(const QPixmap &pic, int id, QMenu *popup, const QString&) { DMultiTabBarButton* const btn = new DMultiTabBarButton(pic, QString(), id, this); // a button with a QMenu can have another size. Make sure the button has always the same size. btn->setFixedWidth(btn->height()); btn->setMenu(popup); d->buttons.append(btn); d->layout->insertWidget(0,btn); btn->show(); d->btnTabSep->show(); return 0; } void DMultiTabBar::updateSeparator() { bool hideSep = true; QListIterator it(d->buttons); while (it.hasNext()) { if (it.next()->isVisibleTo(this)) { hideSep = false; break; } } if (hideSep) { d->btnTabSep->hide(); } else { d->btnTabSep->show(); } } int DMultiTabBar::appendTab(const QPixmap& pic, int id, const QString& text) { d->internal->appendTab(pic,id,text); return 0; } DMultiTabBarButton* DMultiTabBar::button(int id) const { QListIterator it(d->buttons); while (it.hasNext()) { DMultiTabBarButton* const button = it.next(); if (button->id() == id) { return button; } } return nullptr; } DMultiTabBarTab* DMultiTabBar::tab(int id) const { return d->internal->tab(id); } void DMultiTabBar::removeButton(int id) { for (int pos = 0 ; pos < d->buttons.count() ; ++pos) { if (d->buttons.at(pos)->id() == id) { d->buttons.takeAt(pos)->deleteLater(); break; } } if (d->buttons.count() == 0) { d->btnTabSep->hide(); } } void DMultiTabBar::removeTab(int id) { d->internal->removeTab(id); } void DMultiTabBar::setTab(int id,bool state) { DMultiTabBarTab* const ttab = tab(id); if (ttab) { ttab->setState(state); } } bool DMultiTabBar::isTabRaised(int id) const { DMultiTabBarTab* const ttab = tab(id); if (ttab) { return ttab->isChecked(); } return false; } void DMultiTabBar::setStyle(TextStyle style) { d->internal->setStyle(style); } DMultiTabBar::TextStyle DMultiTabBar::tabStyle() const { return d->internal->d->style; } void DMultiTabBar::setPosition(Qt::Edge pos) { d->position = pos; d->internal->setPosition(pos); } Qt::Edge DMultiTabBar::position() const { return d->position; } void DMultiTabBar::fontChange(const QFont&) { updateGeometry(); } // ------------------------------------------------------------------------------------- class Q_DECL_HIDDEN SidebarState { public: SidebarState() : activeWidget(nullptr), size(0) { } SidebarState(QWidget* const w, int size) : activeWidget(w), size(size) { } QWidget* activeWidget; int size; }; // ------------------------------------------------------------------------------------- class Q_DECL_HIDDEN Sidebar::Private { public: explicit Private() : minimizedDefault(false), minimized(false), isMinimized(false), tabs(0), activeTab(-1), dragSwitchId(-1), restoreSize(0), stack(nullptr), splitter(nullptr), dragSwitchTimer(nullptr), appendedTabsStateCache(), optionActiveTabEntry(QLatin1String("ActiveTab")), optionMinimizedEntry(QLatin1String("Minimized")), optionRestoreSizeEntry(QLatin1String("RestoreSize")) { } bool minimizedDefault; bool minimized; /** * Backup of shrinked status before backup(), restored by restore() * NOTE: when sidebar is hidden, only icon bar is affected. If sidebar view is * visible, this one must be shrink and restored accordingly. */ bool isMinimized; int tabs; int activeTab; int dragSwitchId; int restoreSize; QStackedWidget* stack; SidebarSplitter* splitter; QTimer* dragSwitchTimer; QHash appendedTabsStateCache; const QString optionActiveTabEntry; const QString optionMinimizedEntry; const QString optionRestoreSizeEntry; }; class Q_DECL_HIDDEN SidebarSplitter::Private { public: QList sidebars; }; // ------------------------------------------------------------------------------------- Sidebar::Sidebar(QWidget* const parent, SidebarSplitter* const sp, Qt::Edge side, bool minimizedDefault) : DMultiTabBar(side, parent), StateSavingObject(this), d(new Private) { d->splitter = sp; d->minimizedDefault = minimizedDefault; d->stack = new QStackedWidget(d->splitter); d->dragSwitchTimer = new QTimer(this); connect(d->dragSwitchTimer, SIGNAL(timeout()), this, SLOT(slotDragSwitchTimer())); d->splitter->d->sidebars << this; setStyle(DMultiTabBar::ActiveIconText); } Sidebar::~Sidebar() { saveState(); if (d->splitter) { d->splitter->d->sidebars.removeAll(this); } delete d; } SidebarSplitter* Sidebar::splitter() const { return d->splitter; } void Sidebar::doLoadState() { KConfigGroup group = getConfigGroup(); int tab = group.readEntry(entryName(d->optionActiveTabEntry), 0); bool minimized = group.readEntry(entryName(d->optionMinimizedEntry), d->minimizedDefault); d->restoreSize = group.readEntry(entryName(d->optionRestoreSizeEntry), -1); // validate if ((tab >= d->tabs) || (tab < 0)) { tab = 0; } if (minimized) { d->activeTab = tab; setTab(d->activeTab, false); d->stack->setCurrentIndex(d->activeTab); shrink(); emit signalChangedTab(d->stack->currentWidget()); return; } d->activeTab = -1; clicked(tab); } void Sidebar::doSaveState() { KConfigGroup group = getConfigGroup(); group.writeEntry(entryName(d->optionActiveTabEntry), d->activeTab); group.writeEntry(entryName(d->optionMinimizedEntry), d->minimized); group.writeEntry(entryName(d->optionRestoreSizeEntry), d->minimized ? d->restoreSize : -1); } void Sidebar::backup() { // backup preview state of sidebar view (shrink or not) d->isMinimized = d->minimized; // In all case, shrink sidebar view shrink(); DMultiTabBar::hide(); } void Sidebar::backup(const QList thirdWidgetsToBackup, QList* const sizes) { sizes->clear(); foreach (QWidget* const widget, thirdWidgetsToBackup) { *sizes << d->splitter->size(widget); } backup(); } void Sidebar::restore() { DMultiTabBar::show(); // restore preview state of sidebar view, stored in backup() if (!d->isMinimized) { expand(); } } void Sidebar::restore(const QList thirdWidgetsToRestore, const QList& sizes) { restore(); if (thirdWidgetsToRestore.size() == sizes.size()) { for (int i = 0 ; i < thirdWidgetsToRestore.size() ; ++i) { d->splitter->setSize(thirdWidgetsToRestore.at(i), sizes.at(i)); } } } void Sidebar::appendTab(QWidget* const w, const QIcon& pic, const QString& title) { // Store state (but not on initialization) if (isVisible()) { d->appendedTabsStateCache[w] = SidebarState(d->stack->currentWidget(), d->splitter->size(this)); } // Add tab w->setParent(d->stack); DMultiTabBar::appendTab(pic.pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize)), d->tabs, title); d->stack->insertWidget(d->tabs, w); tab(d->tabs)->setAcceptDrops(true); tab(d->tabs)->installEventFilter(this); connect(tab(d->tabs), SIGNAL(clicked(int)), this, SLOT(clicked(int))); d->tabs++; } void Sidebar::deleteTab(QWidget* const w) { int tab = d->stack->indexOf(w); if (tab < 0) { return; } bool removingActiveTab = (tab == d->activeTab); if (removingActiveTab) { d->activeTab = -1; } d->stack->removeWidget(d->stack->widget(tab)); // delete widget removeTab(tab); d->tabs--; // restore or reset active tab and width if (!d->minimized) { // restore to state before adding tab // using a hash is simple, but does not handle well multiple add/removal operations at a time SidebarState state = d->appendedTabsStateCache.take(w); if (state.activeWidget) { - int tab = d->stack->indexOf(state.activeWidget); + int atab = d->stack->indexOf(state.activeWidget); - if (tab != -1) + if (atab != -1) { - switchTabAndStackToTab(tab); + switchTabAndStackToTab(atab); emit signalChangedTab(d->stack->currentWidget()); if (state.size == 0) { d->minimized = true; setTab(d->activeTab, false); } d->splitter->setSize(this, state.size); } } else { if (removingActiveTab) { clicked(d->tabs - 1); } d->splitter->setSize(this, -1); } } else { d->restoreSize = -1; } } void Sidebar::clicked(int tab) { if ((tab >= d->tabs) || (tab < 0)) { return; } if (tab == d->activeTab) { d->stack->isHidden() ? expand() : shrink(); } else { switchTabAndStackToTab(tab); if (d->minimized) { expand(); } emit signalChangedTab(d->stack->currentWidget()); } } void Sidebar::setActiveTab(QWidget* const w) { int tab = d->stack->indexOf(w); if (tab < 0) { return; } switchTabAndStackToTab(tab); if (d->minimized) { expand(); } emit signalChangedTab(d->stack->currentWidget()); } void Sidebar::activePreviousTab() { int tab = d->stack->indexOf(d->stack->currentWidget()); if (tab == 0) { tab = d->tabs-1; } else { tab--; } setActiveTab(d->stack->widget(tab)); } void Sidebar::activeNextTab() { int tab = d->stack->indexOf(d->stack->currentWidget()); if (tab == d->tabs-1) { tab = 0; } else { tab++; } setActiveTab(d->stack->widget(tab)); } void Sidebar::switchTabAndStackToTab(int tab) { if (d->activeTab >= 0) { setTab(d->activeTab, false); } d->activeTab = tab; setTab(d->activeTab, true); d->stack->setCurrentIndex(d->activeTab); } QWidget* Sidebar::getActiveTab() const { if (d->splitter) { return d->stack->currentWidget(); } else { return nullptr; } } void Sidebar::shrink() { d->minimized = true; // store the size that we had. We may later need it when we restore to visible. int currentSize = d->splitter->size(this); if (currentSize) { d->restoreSize = currentSize; } d->stack->hide(); emit signalViewChanged(); } void Sidebar::expand() { d->minimized = false; d->stack->show(); QTimer::singleShot(0, this, SLOT(slotExpandTimer())); } void Sidebar::slotExpandTimer() { // Do not expand to size 0 (only splitter handle visible) // but either to previous size, or the minimum size hint if (d->splitter->size(this) == 0) { setTab(d->activeTab, true); d->splitter->setSize(this, d->restoreSize ? d->restoreSize : -1); } emit signalViewChanged(); } bool Sidebar::isExpanded() const { return !d->minimized; } bool Sidebar::eventFilter(QObject* obj, QEvent* ev) { for (int i = 0 ; i < d->tabs ; ++i) { if (obj == tab(i)) { if (ev->type() == QEvent::DragEnter) { QDragEnterEvent* const e = static_cast(ev); enterEvent(e); e->accept(); return false; } else if (ev->type() == QEvent::DragMove) { if (!d->dragSwitchTimer->isActive()) { d->dragSwitchTimer->setSingleShot(true); d->dragSwitchTimer->start(800); d->dragSwitchId = i; } return false; } else if (ev->type() == QEvent::DragLeave) { d->dragSwitchTimer->stop(); QDragLeaveEvent* const e = static_cast(ev); leaveEvent(e); return false; } else if (ev->type() == QEvent::Drop) { d->dragSwitchTimer->stop(); QDropEvent* const e = static_cast(ev); leaveEvent(e); return false; } else { return false; } } } // Else, pass the event on to the parent class return DMultiTabBar::eventFilter(obj, ev); } void Sidebar::slotDragSwitchTimer() { clicked(d->dragSwitchId); } void Sidebar::slotSplitterBtnClicked() { clicked(d->activeTab); } // ----------------------------------------------------------------------------- const QString SidebarSplitter::DEFAULT_CONFIG_KEY = QLatin1String("SplitterState"); SidebarSplitter::SidebarSplitter(QWidget* const parent) : QSplitter(parent), d(new Private) { connect(this, SIGNAL(splitterMoved(int,int)), this, SLOT(slotSplitterMoved(int,int))); } SidebarSplitter::SidebarSplitter(Qt::Orientation orientation, QWidget* const parent) : QSplitter(orientation, parent), d(new Private) { connect(this, SIGNAL(splitterMoved(int,int)), this, SLOT(slotSplitterMoved(int,int))); } SidebarSplitter::~SidebarSplitter() { // retreat cautiously from sidebars that live longer foreach (Sidebar* const sidebar, d->sidebars) { sidebar->d->splitter = nullptr; } delete d; } void SidebarSplitter::restoreState(KConfigGroup& group) { restoreState(group, DEFAULT_CONFIG_KEY); } void SidebarSplitter::restoreState(KConfigGroup& group, const QString& key) { if (group.hasKey(key)) { QByteArray state; state = group.readEntry(key, state); QSplitter::restoreState(QByteArray::fromBase64(state)); } } void SidebarSplitter::saveState(KConfigGroup& group) { saveState(group, DEFAULT_CONFIG_KEY); } void SidebarSplitter::saveState(KConfigGroup& group, const QString& key) { group.writeEntry(key, QSplitter::saveState().toBase64()); } int SidebarSplitter::size(Sidebar* const bar) const { return size(bar->d->stack); } int SidebarSplitter::size(QWidget* const widget) const { int index = indexOf(widget); if (index == -1) { return -1; } return sizes().at(index); } void SidebarSplitter::setSize(Sidebar* const bar, int size) { setSize(bar->d->stack, size); } void SidebarSplitter::setSize(QWidget* const widget, int size) { int index = indexOf(widget); if (index == -1) { return; } // special case: Use minimum size hint if (size == -1) { if (orientation() == Qt::Horizontal) { size = widget->minimumSizeHint().width(); } if (orientation() == Qt::Vertical) { size = widget->minimumSizeHint().height(); } } QList sizeList = sizes(); sizeList[index] = size; setSizes(sizeList); } void SidebarSplitter::slotSplitterMoved(int pos, int index) { Q_UNUSED(pos); // When the user moves the splitter so that size is 0 (collapsed), // it may be difficult to restore the sidebar as clicking the buttons // has no effect (only hides/shows the splitter handle) // So we want to transform this size-0-sidebar // to a sidebar that is shrunk (d->stack hidden) // and can be restored by clicking a tab bar button // We need to look at the widget between index-1 and index // and the one between index and index+1 QList sizeList = sizes(); // Is there a sidebar with size 0 before index ? - if (index > 0 && sizeList.at(index-1) == 0) + if ((index > 0) && (sizeList.at(index-1) == 0)) { QWidget* const w = widget(index-1); foreach (Sidebar* const sidebar, d->sidebars) { if (w == sidebar->d->stack) { if (!sidebar->d->minimized) { sidebar->setTab(sidebar->d->activeTab, false); sidebar->shrink(); } break; } } } // Is there a sidebar with size 0 after index ? if (sizeList.at(index) == 0) { QWidget* const w = widget(index); foreach (Sidebar* const sidebar, d->sidebars) { if (w == sidebar->d->stack) { if (!sidebar->d->minimized) { sidebar->setTab(sidebar->d->activeTab, false); sidebar->shrink(); } break; } } } } } // namespace Digikam diff --git a/core/libs/widgets/layout/sidebar.h b/core/libs/widgets/layout/sidebar.h index 6b29528ad5..641bb4c2e5 100644 --- a/core/libs/widgets/layout/sidebar.h +++ b/core/libs/widgets/layout/sidebar.h @@ -1,534 +1,534 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2005-03-22 * Description : a widget to manage sidebar in GUI. * * Copyright (C) 2005-2006 by Joern Ahrens * Copyright (C) 2006-2020 by Gilles Caulier * Copyright (C) 2008-2013 by Marcel Wiesweg * Copyright (C) 2001-2003 by Joseph Wenninger * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_SIDE_BAR_H #define DIGIKAM_SIDE_BAR_H // Qt includes #include #include #include #include #include #include #include // Local includes #include "digikam_export.h" #include "statesavingobject.h" class KConfigGroup; namespace Digikam { class DMultiTabBarButton; class DMultiTabBarTab; /** * A Widget for horizontal and vertical tabs. */ class DIGIKAM_EXPORT DMultiTabBar : public QWidget { Q_OBJECT public: /** * The list of available styles for DMultiTabBar */ enum TextStyle { ActiveIconText = 0, ///< Always shows icon, only show the text of active tabs. AllIconsText = 2 ///< Always shows the text and icons. }; public: explicit DMultiTabBar(Qt::Edge pos, QWidget* const parent=nullptr); virtual ~DMultiTabBar(); /** * append a new button to the button area. The button can later on be accessed with button(ID) * eg for connecting signals to it * @param pic a pixmap for the button * @param id an arbitrary ID value. It will be emitted in the clicked signal for identifying the button * if more than one button is connected to a signals. * @param popup A popup menu which should be displayed if the button is clicked * @param not_used_yet will be used for a popup text in the future */ int appendButton(const QPixmap &pic, int id=-1, QMenu* const popup=nullptr, const QString& not_used_yet=QString()); /** * remove a button with the given ID */ void removeButton(int id); /** * append a new tab to the tab area. It can be accessed later on with tabb(id); * @param pic a bitmap for the tab * @param id an arbitrary ID which can be used later on to identify the tab * @param text if a mode with text is used it will be the tab text, otherwise a mouse over hint */ int appendTab(const QPixmap& pic,int id=-1,const QString& text=QString()); /** * remove a tab with a given ID */ void removeTab(int id); /** * set a tab to "raised" * @param id The ID of the tab to manipulate * @param state true == activated/raised, false == not active */ void setTab(int id, bool state); /** * return the state of a tab, identified by its ID */ bool isTabRaised(int id) const; /** * get a pointer to a button within the button area identified by its ID */ DMultiTabBarButton* button(int id) const; /** * get a pointer to a tab within the tab area, identified by its ID */ DMultiTabBarTab* tab(int id) const; /** * set the real position of the widget. * @param pos if the mode is horizontal, only use top, bottom, if it is vertical use left or right */ void setPosition(Qt::Edge pos); /** * get the tabbar position. * @return position */ Qt::Edge position() const; /** * set the display style of the tabs */ void setStyle(TextStyle style); /** * get the display style of the tabs * @return display style */ TextStyle tabStyle() const; protected: void updateSeparator(); virtual void fontChange(const QFont&); private: friend class DMultiTabBarButton; class Private; Private* const d; }; // ------------------------------------------------------------------------------------- class DIGIKAM_EXPORT DMultiTabBarButton: public QPushButton { Q_OBJECT public: int id() const; virtual ~DMultiTabBarButton(); public Q_SLOTS: void setText(const QString& text); Q_SIGNALS: /** * this is emitted if the button is clicked * @param id the ID identifying the button */ void clicked(int id); protected Q_SLOTS: virtual void slotClicked(); protected: DMultiTabBarButton(const QPixmap& pic, const QString&, int id, QWidget* const parent); virtual void hideEvent(QHideEvent*) override; virtual void showEvent(QShowEvent*) override; virtual void paintEvent(QPaintEvent*) override; private: friend class DMultiTabBar; int m_id; }; // ------------------------------------------------------------------------------------- class DIGIKAM_EXPORT DMultiTabBarTab: public DMultiTabBarButton { Q_OBJECT public: virtual ~DMultiTabBarTab(); virtual QSize sizeHint() const override; virtual QSize minimumSizeHint() const override; public Q_SLOTS: /** * this is used internally, but can be used by the user. * It the according call of DMultiTabBar is invoked though this modifications will be overwritten */ void setPosition(Qt::Edge); /** * this is used internally, but can be used by the user. * It the according call of DMultiTabBar is invoked though this modifications will be overwritten */ void setStyle(DMultiTabBar::TextStyle); /** * set the active state of the tab * @param state true==active false==not active */ void setState(bool state); void setIcon(const QString&); void setIcon(const QPixmap&); protected: void computeMargins (int* hMargin, int* vMargin) const; QSize computeSizeHint(bool withText) const; bool shouldDrawText() const; bool isVertical() const; QPixmap iconPixmap() const; void initStyleOption(QStyleOptionToolButton* opt) const; friend class DMultiTabBarFrame; /** * This class should never be created except with the appendTab call of DMultiTabBar */ DMultiTabBarTab(const QPixmap& pic, const QString&, int id, QWidget* const parent, Qt::Edge pos, DMultiTabBar::TextStyle style); virtual void paintEvent(QPaintEvent*) override; private: class Private; Private* const d; }; // ------------------------------------------------------------------------------------- class DMultiTabBarFrame: public QFrame { Q_OBJECT public: explicit DMultiTabBarFrame(QWidget* const parent, Qt::Edge pos); virtual ~DMultiTabBarFrame(); int appendTab(const QPixmap&, int = -1, const QString& = QString()); DMultiTabBarTab* tab(int) const; void removeTab(int); void setPosition(Qt::Edge pos); void setStyle(DMultiTabBar::TextStyle style); void showActiveTabTexts(bool show); QList* tabs(); protected: /** * Reimplemented from QScrollView * in order to ignore all mouseEvents on the viewport, so that the * parent can handle them. */ virtual void contentsMousePressEvent(QMouseEvent*); virtual void mousePressEvent(QMouseEvent*) override; private: friend class DMultiTabBar; class Private; Private* const d; }; // ------------------------------------------------------------------------------------- class SidebarSplitter; /** * This class handles a sidebar view * * Since this class derives from StateSavingObject, you can call * StateSavingObject#loadState() and StateSavingObject#saveState() * for loading/saving of settings. However, if you use multiple * sidebar instances in your program, you have to remember to either * call QObject#setObjectName(), StateSavingObject#setEntryPrefix() or * StateSavingObject#setConfigGroup() first. */ class DIGIKAM_EXPORT Sidebar : public DMultiTabBar, public StateSavingObject { Q_OBJECT public: /** * Creates a new sidebar * @param parent sidebar's parent * @param sp sets the splitter, which should handle the width. The splitter normally * is part of the main view. Internally, the width of the widget stack can * be changed by a QSplitter. * @param side where the sidebar should be displayed. At the left or right border. - Use Qt::LeftEdge or Qt::RightEdge. + * Use Qt::LeftEdge or Qt::RightEdge. * @param minimizedDefault hide the sidebar when the program is started the first time. */ explicit Sidebar(QWidget* const parent, SidebarSplitter* const sp, Qt::Edge side = Qt::LeftEdge, bool minimizedDefault=false); virtual ~Sidebar(); SidebarSplitter* splitter() const; /** * Appends a new tab to the sidebar * @param w widget which is activated by this tab * @param pic icon which is shown in this tab * @param title text which is shown it this tab */ void appendTab(QWidget* const w, const QIcon& pic, const QString& title); /** * Deletes a tab from the tabbar */ void deleteTab(QWidget* const w); /** * Activates a tab */ void setActiveTab(QWidget* const w); /** * Activates a next tab from current one. If current one is last, first one is activated. */ void activeNextTab(); /** * Activates a previous tab from current one. If current one is first, last one is activated. */ void activePreviousTab(); /** * Returns the currently activated tab, or 0 if no tab is active - */ + */ QWidget* getActiveTab() const; /** * Hides the sidebar (display only the activation buttons) */ void shrink(); /** * Redisplays the whole sidebar */ void expand(); /** * Hide sidebar and backup minimized state. */ void backup(); /** * Hide sidebar and backup minimized state. * If there are other widgets in this splitter, stores * their sizes in the provided list. */ void backup(const QList thirdWidgetsToBackup, QList* const sizes); /** * Show sidebar and restore minimized state. */ void restore(); /** * Show sidebar and restore minimized state. * Restores other widgets' sizes in splitter. */ void restore(const QList thirdWidgetsToRestore, const QList& sizes); /** * Return the visible status of current sidebar tab. */ bool isExpanded() const; protected: /** * Load the last view state from disk - called by StateSavingObject#loadState() */ void doLoadState() override; /** * Save the view state to disk - called by StateSavingObject#saveState() */ void doSaveState() override; private: bool eventFilter(QObject* o, QEvent* e) override; void switchTabAndStackToTab(int tab); private Q_SLOTS: /** * Activates a tab */ void clicked(int tab); void slotExpandTimer(); void slotDragSwitchTimer(); void slotSplitterBtnClicked(); Q_SIGNALS: /** * Is emitted, when another tab is activated */ void signalChangedTab(QWidget* w); /** * Is emitted, when tab is shrink or expanded */ void signalViewChanged(); private: friend class SidebarSplitter; class Private; Private* const d; }; // ----------------------------------------------------------------------------- class DIGIKAM_EXPORT SidebarSplitter : public QSplitter { Q_OBJECT public: const static QString DEFAULT_CONFIG_KEY; /** * This is a QSplitter with better support for storing its state * in config files, especially if Sidebars are contained in the splitter. */ explicit SidebarSplitter(QWidget* const parent = nullptr); explicit SidebarSplitter(Qt::Orientation orientation, QWidget* const parent = nullptr); ~SidebarSplitter(); /** * Saves the splitter state to group, handling minimized sidebars correctly. * DEFAULT_CONFIG_KEY is used for storing the state. */ void saveState(KConfigGroup& group); /** * Saves the splitter state to group, handling minimized sidebars correctly. * This version uses a specified key in the config group. */ void saveState(KConfigGroup& group, const QString& key); /** * Restores the splitter state from group, handling minimized sidebars correctly. * DEFAULT_CONFIG_KEY is used for restoring the state. */ void restoreState(KConfigGroup& group); /** * Restores the splitter state from group, handling minimized sidebars correctly. * This version uses a specified key in the config group. */ void restoreState(KConfigGroup& group, const QString& key); /** * Returns the value of sizes() that corresponds to the given Sidebar or splitter child widget. */ int size(Sidebar* const bar) const; int size(QWidget* const widget) const; /** * Sets the splitter size for the given sidebar or splitter child widget to size. * Special value -1: Sets the minimum size hint of the widget. */ void setSize(Sidebar* const bar, int size); void setSize(QWidget* const widget, int size); void addSplitterCollapserButton(QWidget* const widget); private Q_SLOTS: void slotSplitterMoved(int pos, int index); private: friend class Sidebar; class Private; Private* const d; }; } // namespace Digikam #endif // DIGIKAM_SIDE_BAR_H