This adds a new selection effect that is similar to what we have in QtQuick file item views.
There are also changes to some usability: Instead of only the icon and text being the clickable area in icon and details mode, the whole selection is now the clickable area.
Otherwise the usability should stay the same, it's mostly a visual change.
See also: https://invent.kde.org/teams/vdg/issues/-/issues/94
return false;
}
+ for (KItemListWidget *widget : m_view->visibleItemListWidgets()) {
+ widget->setPressed(false);
+ }
+
if (m_view->m_tapAndHoldIndicator->isActive()) {
m_view->m_tapAndHoldIndicator->setActive(false);
}
// we also unhover any old expansion toggle hovers, in case the mouse movement from expansion toggle to icon+text is too fast (i.e. newHoveredWidget is never null between the transition)
unhoverOldExpansionWidget();
- const bool isOverIconAndText = newHoveredWidget->iconRect().contains(mappedPos) || newHoveredWidget->textRect().contains(mappedPos);
+ const bool isOverIconAndText = newHoveredWidget->selectionRectCore().contains(mappedPos);
const bool hasMultipleSelection = m_selectionManager->selectedItems().count() > 1;
if (hasMultipleSelection && !isOverIconAndText) {
const auto widgets = m_view->visibleItemListWidgets();
for (KItemListWidget *widget : widgets) {
+ widget->setPressed(false);
if (widget->isHovered()) {
widget->setHovered(false);
Q_EMIT itemUnhovered(widget->index());
const QRectF widgetRect = m_view->itemRect(index);
if (widgetRect.intersects(rubberBandRect)) {
// Select the full row intersecting with the rubberband rectangle
- const QRectF selectionRect = widget->selectionRect().translated(widgetRect.topLeft());
- const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft());
- if (selectionRect.intersects(rubberBandRect) || iconRect.intersects(rubberBandRect)) {
+ const QRectF selectionRect = widget->selectionRectFull().translated(widgetRect.topLeft());
+ if (selectionRect.intersects(rubberBandRect)) {
selectedItems.insert(index);
}
}
const auto widgets = m_view->visibleItemListWidgets();
for (KItemListWidget *widget : widgets) {
const QPointF mappedPos = widget->mapFromItem(m_view, pos);
- if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
+ if (widget->contains(mappedPos)) {
return widget;
}
}
const auto widgets = m_view->visibleItemListWidgets();
for (KItemListWidget *widget : widgets) {
const QPointF mappedPos = widget->mapFromItem(m_view, pos);
- if (widget->contains(mappedPos)) {
+ if (widget->selectionRectCore().contains(mappedPos)) {
return widget;
}
}
// simplify the overall logic and possibilities both for users and devs.
const bool leftClick = buttons & Qt::LeftButton;
const bool rightClick = buttons & Qt::RightButton;
+ const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick);
// The previous selection is cleared if either
// 1. The selection mode is SingleSelection, or
if (selectedItemsCount > 1 && m_pressedIndex.has_value()) {
const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
const auto mappedPos = row->mapFromItem(m_view, pos);
- if (pressedItemAlreadySelected || row->iconRect().contains(mappedPos) || row->textRect().contains(mappedPos)) {
+ if (row->selectionRectCore().contains(mappedPos)) {
// we are indeed inside the text/icon rect, keep m_pressedIndex what it is
// and short-circuit for single-click activation (it will then propagate to onRelease and activate the item)
// or we just keep going for double-click activation
- if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) {
+ if (singleClickActivation || m_singleClickActivationEnforced) {
if (!pressedItemAlreadySelected) {
// An unselected item was clicked directly while deselecting multiple other items so we mark it "current".
m_selectionManager->setCurrentItem(m_pressedIndex.value());
m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
}
}
+ if (leftClick) {
+ row->setPressed(true);
+ }
return true; // event handled, don't create rubber band
}
} else {
if (m_pressedIndex.has_value()) {
// The hover highlight area of an item is being pressed.
const auto row = m_view->m_visibleItems.value(m_pressedIndex.value()); // anything outside of row.contains() will be the empty region of the row rect
- const bool hitTargetIsRowEmptyRegion = !row->contains(row->mapFromItem(m_view, pos));
+
+ const bool hitTargetIsRowEmptyRegion = !row->selectionRectCore().contains(row->mapFromItem(m_view, pos));
// again, when this method returns false, a rubberBand selection is created as the event is not consumed;
// createRubberBand here tells us whether to return true or false.
bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
+ if (leftClick) {
+ row->setPressed(true);
+ }
+
if (rightClick && hitTargetIsRowEmptyRegion) {
// We have a right click outside the icon and text rect but within the hover highlight area.
// We don't want items to get selected through this, so we return now.
break;
case SingleSelection:
- if (!leftClick || shiftOrControlPressed
- || (!m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) && !m_singleClickActivationEnforced)) {
+ if (!leftClick || shiftOrControlPressed || (!singleClickActivation && !m_singleClickActivationEnforced)) {
m_selectionManager->setSelected(m_pressedIndex.value());
}
break;
// We rule out the latter, if the item is not clicked directly and was unselected previously.
const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
const auto mappedPos = row->mapFromItem(m_view, pos);
- if (!row->iconRect().contains(mappedPos) && !row->textRect().contains(mappedPos) && !pressedItemAlreadySelected) {
+ if (!row->selectionRectCore().contains(mappedPos)) {
createRubberBand = true;
} else {
m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
}
} else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
// Select the pressed item and start a new anchored selection
- if (!leftClick || shiftOrControlPressed
- || (!m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) && !m_singleClickActivationEnforced)) {
+ if (!leftClick || shiftOrControlPressed || (!singleClickActivation && !m_singleClickActivationEnforced)) {
m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
}
m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
const KItemListWidget *widget = it.value();
const QPointF mappedPos = widget->mapFromItem(this, pos);
- if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
+ if (widget->contains(mappedPos)) {
return it.key();
}
}
const KItemListWidget *widget = m_visibleItems.value(index);
if (widget) {
- contextRect = widget->iconRect() | widget->textRect();
+ contextRect = widget->selectionRectCore();
contextRect.translate(itemRect(index).topLeft());
}
, m_expansionAreaHovered(false)
, m_alternateBackground(false)
, m_enabledSelectionToggle(false)
+ , m_clickHighlighted(false)
, m_data()
, m_visibleRoles()
, m_columnWidths()
painter->fillRect(backgroundRect, backgroundColor);
}
- if (m_selected && m_editedRole.isEmpty()) {
+ if ((m_selected || m_current) && m_editedRole.isEmpty()) {
const QStyle::State activeState(isActiveWindow() && widget->hasFocus() ? QStyle::State_Active : 0);
drawItemStyleOption(painter, widget, activeState | QStyle::State_Enabled | QStyle::State_Selected | QStyle::State_Item);
}
- if (m_current && m_editedRole.isEmpty()) {
- QStyleOptionFocusRect focusRectOption;
- initStyleOption(&focusRectOption);
- focusRectOption.rect = textFocusRect().toRect();
- focusRectOption.state = QStyle::State_Enabled | QStyle::State_Item | QStyle::State_KeyboardFocusChange;
- if (m_selected && widget->hasFocus()) {
- focusRectOption.state |= QStyle::State_Selected;
- }
-
- style()->drawPrimitive(QStyle::PE_FrameFocusRect, &focusRectOption, painter, widget);
- }
-
if (m_hoverOpacity > 0.0) {
if (!m_hoverCache) {
// Initialize the m_hoverCache pixmap to improve the drawing performance
return false;
}
- return iconRect().contains(point) || textRect().contains(point) || expansionToggleRect().contains(point) || selectionToggleRect().contains(point);
+ return selectionRectFull().contains(point) || expansionToggleRect().contains(point);
}
QRectF KItemListWidget::textFocusRect() const
m_hoverCache = nullptr;
}
+bool KItemListWidget::isPressed() const
+{
+ return m_clickHighlighted;
+}
+
+void KItemListWidget::setPressed(bool enabled)
+{
+ if (m_clickHighlighted != enabled) {
+ m_clickHighlighted = enabled;
+ clearHoverCache();
+ update();
+ }
+}
+
void KItemListWidget::drawItemStyleOption(QPainter *painter, QWidget *widget, QStyle::State styleState)
{
QStyleOptionViewItem viewItemOption;
+ constexpr int roundness = 5; // From Breeze style.
+ constexpr qreal penWidth = 1.5;
initStyleOption(&viewItemOption);
viewItemOption.state = styleState;
viewItemOption.viewItemPosition = QStyleOptionViewItem::OnlyOne;
viewItemOption.showDecorationSelected = true;
- viewItemOption.rect = selectionRect().toRect();
- style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &viewItemOption, painter, widget);
+ viewItemOption.rect = selectionRectFull().toRect();
+ QPainterPath path;
+ path.addRoundedRect(selectionRectFull().adjusted(penWidth, penWidth, -penWidth, -penWidth), roundness, roundness);
+ QColor backgroundColor{widget->palette().color(QPalette::Accent)};
+ painter->setRenderHint(QPainter::Antialiasing);
+ bool current = m_current && styleState & QStyle::State_Active;
+
+ // Background item, alpha values are from
+ // https://invent.kde.org/plasma/libplasma/-/blob/master/src/desktoptheme/breeze/widgets/viewitem.svg
+ backgroundColor.setAlphaF(0.0);
+
+ if (m_clickHighlighted) {
+ backgroundColor.setAlphaF(1.0);
+ } else {
+ if (m_selected && m_hovered) {
+ backgroundColor.setAlphaF(0.40);
+ } else if (m_selected) {
+ backgroundColor.setAlphaF(0.32);
+ } else if (m_hovered) {
+ backgroundColor = widget->palette().color(QPalette::Text);
+ backgroundColor.setAlphaF(0.06);
+ }
+ }
+
+ painter->fillPath(path, backgroundColor);
+
+ // Focus decoration
+ if (current) {
+ QColor focusColor{widget->palette().color(QPalette::Accent)};
+ focusColor = m_styleOption.palette.color(QPalette::Base).lightnessF() > 0.5 ? focusColor.darker(110) : focusColor.lighter(110);
+ focusColor.setAlphaF(m_selected || m_hovered ? 0.8 : 0.6);
+ // Set the pen color lighter or darker depending on background color
+ QPen pen{focusColor, penWidth};
+ pen.setCosmetic(true);
+ painter->setPen(pen);
+ painter->drawPath(path);
+ }
}
#include "moc_kitemlistwidget.cpp"
void setHovered(bool hovered);
bool isHovered() const;
+ /** Sets a purely visual pressed highlight effect. */
+ void setPressed(bool enabled);
+ bool isPressed() const;
+
void setExpansionAreaHovered(bool hover);
bool expansionAreaHovered() const;
int iconSize() const;
/**
- * @return True if \a point is inside KItemListWidget::hoverRect(),
- * KItemListWidget::textRect(), KItemListWidget::selectionToggleRect()
+ * @return True if \a point is inside KItemListWidget::selectionRectFull(),
+ * KItemListWidget::selectionToggleRect()
* or KItemListWidget::expansionToggleRect().
* @reimp
*/
bool contains(const QPointF &point) const override;
- /**
- * @return Rectangle for the area that shows the icon.
- */
- virtual QRectF iconRect() const = 0;
-
/**
* @return Rectangle for the area that contains the text-properties.
*/
virtual QRectF textFocusRect() const;
/**
- * @return Rectangle around which a selection box should be drawn if the item is selected.
+ * Used for drawing the visuals, and situations where we want the behavior of the
+ * selection to match the visuals.
+ *
+ * @return The rectangle around selection.
+ */
+ virtual QRectF selectionRectFull() const = 0;
+
+ /**
+ * @return The core area of the item. All of it reacts exactly the same way to mouse clicks.
*/
- virtual QRectF selectionRect() const = 0;
+ virtual QRectF selectionRectCore() const = 0;
/**
* @return Rectangle for the selection-toggle that is used to select or deselect an item.
bool m_expansionAreaHovered;
bool m_alternateBackground;
bool m_enabledSelectionToggle;
+ bool m_clickHighlighted;
QHash<QByteArray, QVariant> m_data;
QList<QByteArray> m_visibleRoles;
QHash<QByteArray, qreal> m_columnWidths;
, m_scaledPixmapSize()
, m_columnWidthSum()
, m_iconRect()
- , m_hoverPixmap()
, m_textRect()
, m_sortedVisibleRoles()
, m_expansionArea()
drawSiblingsInformation(painter);
}
- auto pixmap = isHovered() ? m_hoverPixmap : m_pixmap;
+ auto pixmap = m_pixmap;
if (!m_overlays.isEmpty()) {
const qreal dpr = KItemViewsUtils::devicePixelRatio(this);
{
QPainter p(&pixmap2);
p.setOpacity(hoverOpacity());
- p.drawPixmap(0, 0, m_hoverPixmap);
+ p.drawPixmap(0, 0, m_pixmap);
}
// Paint pixmap2 on pixmap1 using CompositionMode_Plus
#endif
}
-QRectF KStandardItemListWidget::iconRect() const
-{
- const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
- return m_iconRect;
-}
-
QRectF KStandardItemListWidget::textRect() const
{
const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
return m_textRect;
}
-QRectF KStandardItemListWidget::selectionRect() const
+QRectF KStandardItemListWidget::selectionRectFull() const
{
const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
-
- switch (m_layout) {
- case IconsLayout:
- return m_textRect;
-
- case CompactLayout:
- case DetailsLayout: {
- const int padding = styleOption().padding;
- QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding);
- QRectF result = adjustedIconRect | m_textRect;
+ const int padding = styleOption().padding;
+ if (m_layout == DetailsLayout) {
+ auto rect = m_iconRect | m_textRect;
if (m_highlightEntireRow) {
if (layoutDirection() == Qt::LeftToRight) {
- result.setRight(leftPadding() + m_columnWidthSum);
+ rect.setRight(leftPadding() + m_columnWidthSum);
} else {
- result.setLeft(size().width() - m_columnWidthSum - rightPadding());
+ rect.setLeft(size().width() - m_columnWidthSum - rightPadding());
}
}
- return result;
+ return rect.adjusted(-padding, 0, padding, 0);
+ } else {
+ if (m_layout == CompactLayout) {
+ return rect().adjusted(-padding, 0, padding, 0);
+ }
+ return rect();
}
+}
- default:
- Q_ASSERT(false);
- break;
+QRectF KStandardItemListWidget::selectionRectCore() const
+{
+ // Allow dragging from selection area in details view.
+ if (m_layout == DetailsLayout && highlightEntireRow() && !isSelected()) {
+ QRectF result = m_iconRect | m_textRect;
+ return result;
}
-
- return m_textRect;
+ return selectionRectFull();
}
QRectF KStandardItemListWidget::expansionToggleRect() const
{
const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
- const QRectF widgetIconRect = iconRect();
const int widgetIconSize = iconSize();
int toggleSize = KIconLoader::SizeSmall;
if (widgetIconSize >= KIconLoader::SizeEnormous) {
toggleSize = KIconLoader::SizeSmallMedium;
}
- QPointF pos = widgetIconRect.topLeft();
-
- // If the selection toggle has a very small distance to the
- // widget borders, the size of the selection toggle will get
- // increased to prevent an accidental clicking of the item
- // when trying to hit the toggle.
- const int widgetHeight = size().height();
- const int widgetWidth = size().width();
- const int minMargin = 2;
-
- if (toggleSize + minMargin * 2 >= widgetHeight) {
- pos.rx() -= (widgetHeight - toggleSize) / 2;
- toggleSize = widgetHeight;
- pos.setY(0);
- }
- if (toggleSize + minMargin * 2 >= widgetWidth) {
- pos.ry() -= (widgetWidth - toggleSize) / 2;
- toggleSize = widgetWidth;
- pos.setX(0);
- }
-
+ const int padding = styleOption().padding;
+ const QRectF selectionRectMinusPadding = selectionRectFull().adjusted(padding, padding, -padding, -padding);
+ QPointF pos = selectionRectMinusPadding.topLeft();
if (QApplication::isRightToLeft()) {
- pos.setX(widgetIconRect.right() - (pos.x() + toggleSize - widgetIconRect.left()));
+ pos.setX(selectionRectMinusPadding.right() - (pos.x() + toggleSize - selectionRectMinusPadding.left()));
}
return QRectF(pos, QSizeF(toggleSize, toggleSize));
QPalette::ColorRole KStandardItemListWidget::normalTextColorRole() const
{
- return QPalette::Text;
+ if (isPressed()) {
+ return QPalette::HighlightedText;
+ } else {
+ return QPalette::Text;
+ }
}
void KStandardItemListWidget::setTextColor(const QColor &color)
}
const QPalette::ColorGroup group = isActiveWindow() && widget.hasFocus() ? QPalette::Active : QPalette::Inactive;
- const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : normalTextColorRole();
+ const QPalette::ColorRole role = normalTextColorRole();
return styleOption().palette.color(group, role);
}
}
if (m_pixmap.isNull()) {
- m_hoverPixmap = QPixmap();
return;
}
if (m_isHidden) {
KIconEffect::semiTransparent(m_pixmap);
}
-
- if (m_layout == IconsLayout && isSelected()) {
- const QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color();
- QImage image = m_pixmap.toImage();
- if (image.isNull()) {
- m_hoverPixmap = QPixmap();
- return;
- }
- KIconEffect::colorize(image, color, 0.8f);
- m_pixmap = QPixmap::fromImage(image);
- }
}
int scaledIconSize = 0;
const QSizeF squareIconSize(widgetIconSize, widgetIconSize);
m_iconRect = QRectF(squareIconPos, squareIconSize);
}
-
- // Prepare the pixmap that is used when the item gets hovered
- if (isHovered()) {
- m_hoverPixmap = m_pixmap;
- KIconEffect::toActive(m_hoverPixmap);
- } else if (hoverOpacity() <= 0.0) {
- // No hover animation is ongoing. Clear m_hoverPixmap to save memory.
- m_hoverPixmap = QPixmap();
- }
}
void KStandardItemListWidget::updateTextsCache()
} else if (isSelected() && hasFocus && (m_layout != DetailsLayout || m_highlightEntireRow)) {
// The detail text color needs to match the main text (HighlightedText) for the same level
// of readability. We short circuit early here to avoid interpolating with another color.
- m_additionalInfoTextColor = styleOption().palette.color(QPalette::HighlightedText);
+ m_additionalInfoTextColor = styleOption().palette.color(normalTextColorRole());
return;
} else {
c1 = styleOption().palette.text().color();
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
- QRectF iconRect() const override;
QRectF textRect() const override;
QRectF textFocusRect() const override;
- QRectF selectionRect() const override;
+ QRectF selectionRectFull() const override;
+ QRectF selectionRectCore() const override;
QRectF expansionToggleRect() const override;
QRectF selectionToggleRect() const override;
QPixmap createDragPixmap(const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
qreal m_columnWidthSum;
QRectF m_iconRect; // Cache for KItemListWidget::iconRect()
- QPixmap m_hoverPixmap; // Cache for modified m_pixmap when hovering the item
QRectF m_textRect;
itemHeight = padding * 3 + iconSize + option.fontMetrics.lineSpacing();
- horizontalMargin = 4;
- verticalMargin = 8;
+ const auto margin = style()->pixelMetric(QStyle::PM_SizeGripSize);
+ horizontalMargin = margin;
+ verticalMargin = margin;
maxTextLines = IconsModeSettings::maximumTextLines();
break;
}