return false;
}
- if (m_pressedIndex.has_value()) {
+ if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) {
// Check whether a dragging should be started
if (event->buttons() & Qt::LeftButton) {
const QPointF pos = transform.map(event->pos());
return false;
}
- KItemListWidget* oldHoveredWidget = hoveredWidget();
- const QPointF pos = transform.map(event->pos());
- KItemListWidget* newHoveredWidget = widgetForPos(pos);
+ // We identify the widget whose expansionArea had been hovered before this hoverMoveEvent() triggered.
+ // we can't use hoveredWidget() here (it handles the icon+text rect, not the expansion rect)
+ // like hoveredWidget(), we find the hovered widget for the expansion rect
+ const auto visibleItemListWidgets = m_view->visibleItemListWidgets();
+ const auto oldHoveredExpansionWidgetIterator = std::find_if(visibleItemListWidgets.begin(), visibleItemListWidgets.end(), [](auto &widget) {
+ return widget->expansionAreaHovered();
+ });
+ const auto oldHoveredExpansionWidget = oldHoveredExpansionWidgetIterator == visibleItemListWidgets.end() ?
+ std::nullopt : std::make_optional(*oldHoveredExpansionWidgetIterator);
- if (oldHoveredWidget != newHoveredWidget) {
- if (oldHoveredWidget) {
+ const auto unhoverOldHoveredWidget = [&]() {
+ if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget) {
+ // handle the text+icon one
oldHoveredWidget->setHovered(false);
Q_EMIT itemUnhovered(oldHoveredWidget->index());
}
+ };
- if (newHoveredWidget) {
- newHoveredWidget->setHovered(true);
- const QPointF mappedPos = newHoveredWidget->mapFromItem(m_view, pos);
- newHoveredWidget->setHoverPosition(mappedPos);
- Q_EMIT itemHovered(newHoveredWidget->index());
+ const auto unhoverOldExpansionWidget = [&]() {
+ if (oldHoveredExpansionWidget) {
+ // then the expansion toggle
+ (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
}
- } else if (oldHoveredWidget) {
- const QPointF mappedPos = oldHoveredWidget->mapFromItem(m_view, pos);
- oldHoveredWidget->setHoverPosition(mappedPos);
- }
+ };
+
+ const QPointF pos = transform.map(event->pos());
+ if (KItemListWidget *newHoveredWidget = widgetForPos(pos); newHoveredWidget) {
+ // something got hovered, work out which part and set hover for the appropriate widget
+ const auto mappedPos = newHoveredWidget->mapFromItem(m_view, pos);
+ const bool isOnExpansionToggle = newHoveredWidget->expansionToggleRect().contains(mappedPos);
+
+ if (isOnExpansionToggle) {
+ // make sure we unhover the old one first if old!=new
+ if (oldHoveredExpansionWidget && *oldHoveredExpansionWidget != newHoveredWidget) {
+ (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
+ }
+ // we also unhover any old icon+text hovers, in case the mouse movement from icon+text to expansion toggle is too fast (i.e. newHoveredWidget is never null between the transition)
+ unhoverOldHoveredWidget();
+
+
+ newHoveredWidget->setExpansionAreaHovered(true);
+ } else {
+ // make sure we unhover the old one first if old!=new
+ if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget && oldHoveredWidget != newHoveredWidget) {
+ oldHoveredWidget->setHovered(false);
+ Q_EMIT itemUnhovered(oldHoveredWidget->index());
+ }
+ // 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 hasMultipleSelection = m_selectionManager->selectedItems().count() > 1;
+ if (hasMultipleSelection && !isOverIconAndText) {
+ // In case we have multiple selections, clicking on any row will deselect the selection.
+ // So, as a visual cue for signalling that clicking anywhere won't select, but clear current highlights,
+ // we disable hover of the *row*(i.e. blank space to the right of the icon+text)
+
+ // (no-op in this branch for masked hover)
+ } else {
+ newHoveredWidget->setHovered(true);
+ newHoveredWidget->setHoverPosition(mappedPos);
+ Q_EMIT itemHovered(newHoveredWidget->index());
+ }
+ }
+ } else {
+ // unhover any currently hovered expansion and text+icon widgets
+ unhoverOldHoveredWidget();
+ unhoverOldExpansionWidget();
+ }
return false;
}
const auto widgets = m_view->visibleItemListWidgets();
for (KItemListWidget* widget : widgets) {
const QPointF mappedPos = widget->mapFromItem(m_view, pos);
-
- const bool hovered = widget->contains(mappedPos) &&
- !widget->expansionToggleRect().contains(mappedPos);
- if (hovered) {
+ if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
return widget;
}
}
const bool shiftPressed = modifiers & Qt::ShiftModifier;
const bool controlPressed = modifiers & Qt::ControlModifier;
+ const bool rightClick = buttons & Qt::RightButton;
// The previous selection is cleared if either
// 1. The selection mode is SingleSelection, or
const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value());
const bool clearSelection = m_selectionBehavior == SingleSelection ||
(!shiftOrControlPressed && !pressedItemAlreadySelected);
+
+
+ // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller.
if (clearSelection) {
+ const int selectedItemsCount = m_selectionManager->selectedItems().count();
m_selectionManager->clearSelection();
+ // clear and bail when we got an existing multi-selection
+ 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)) {
+ // 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) {
+ return true; // event handled, don't create rubber band
+ }
+ } else {
+ // we're not inside the text/icon rect, as we've already cleared the selection
+ // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate
+ m_pressedIndex.reset();
+ // we don't stop event propagation and proceed to create a rubber band and let onRelease
+ // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item)
+ return false;
+ }
+ }
} else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) {
// The user might want to start dragging multiple items, but if he clicks the item
// in order to trigger it instead, the other selected items must be deselected.
m_selectionManager->endAnchoredSelection();
}
- if (buttons & Qt::RightButton) {
+ if (rightClick) {
+
+ // Do header hit check and short circuit before commencing any state changing effects
+ if (m_view->headerBoundaries().contains(pos)) {
+ Q_EMIT headerContextMenuRequested(screenPos);
+ return true;
+ }
+
// Stop rubber band from persisting after right-clicks
KItemListRubberBand* rubberBand = m_view->rubberBand();
if (rubberBand->isActive()) {
if (m_pressedIndex.has_value()) {
m_selectionManager->setCurrentItem(m_pressedIndex.value());
+ 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));
+ // 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 (rightClick && hitTargetIsRowEmptyRegion) {
+ // we got a right click outside the text rect, default to action on the current url and not the pressed item
+ Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
+ return true;
+ }
switch (m_selectionBehavior) {
case NoSelection:
if (controlPressed && !shiftPressed) {
m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ createRubberBand = false; // multi selection, don't propagate any further
} else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
// Select the pressed item and start a new anchored selection
m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
break;
}
- if (buttons & Qt::RightButton) {
+ if (rightClick) {
Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
}
-
- return true;
+ return !createRubberBand;
}
- if (buttons & Qt::RightButton) {
- const QRectF headerBounds = m_view->headerBoundaries();
- if (headerBounds.contains(pos)) {
- Q_EMIT headerContextMenuRequested(screenPos);
- } else {
- Q_EMIT viewContextMenuRequested(screenPos);
- }
+ if (rightClick) {
+ // header right click handling would have been done before this so just normal context
+ // menu here is fine
+ Q_EMIT viewContextMenuRequested(screenPos);
return true;
}
const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier ||
controlPressed;
+ const std::optional<int> index = m_view->itemAt(pos);
+
KItemListRubberBand* rubberBand = m_view->rubberBand();
+ bool rubberBandRelease = false;
if (rubberBand->isActive()) {
disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
rubberBand->setActive(false);
m_oldSelection.clear();
m_view->setAutoScroll(false);
+ rubberBandRelease = true;
+ // We check for actual rubber band drag here: if delta between start and end is less than drag threshold,
+ // then we have a single click on one of the rows
+ if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) {
+ rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag
+ // m_pressedIndex will have no value if we came from a multi-selection clearing onPress
+ // in that case, we don't select anything
+ if (index.has_value() && m_pressedIndex.has_value()) {
+ if (controlPressed && m_selectionBehavior == MultiSelection) {
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
+ } else {
+ m_selectionManager->setSelected(index.value());
+ }
+ if (!m_selectionManager->isAnchoredSelectionActive()) {
+ m_selectionManager->beginAnchoredSelection(index.value());
+ }
+ }
+ }
}
- const std::optional<int> index = m_view->itemAt(pos);
-
if (index.has_value() && index == m_pressedIndex) {
// The release event is done above the same item as the press event
// The mouse click should only update the selection, not trigger the item, except when
// we are in single selection mode
emitItemActivated = false;
- } else if (!(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced)) {
- if (touch) {
- emitItemActivated = true;
+ } else {
+ const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced;
+ if (!singleClickActivation) {
+ emitItemActivated = touch;
} else {
- emitItemActivated = false;
+ // activate on single click only if we didn't come from a rubber band release
+ emitItemActivated = !rubberBandRelease;
}
}
if (emitItemActivated) {
return m_headerWidget->preferredColumnWidth(role);
}
+void KItemListHeader::setLeadingPadding(qreal width){
+ if (m_headerWidget->leadingPadding() != width) {
+ m_headerWidget->setLeadingPadding(width);
+ if (m_headerWidget->automaticColumnResizing()) {
+ m_view->applyAutomaticColumnWidths();
+ }
+ m_view->doLayout(KItemListView::NoAnimation);
+ }
+}
+
+qreal KItemListHeader::leadingPadding() const{
+ return m_headerWidget->leadingPadding();
+}
+
KItemListHeader::KItemListHeader(KItemListView* listView) :
QObject(listView),
m_view(listView)
this, &KItemListHeader::columnWidthChanged);
connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChangeFinished,
this, &KItemListHeader::columnWidthChangeFinished);
+ connect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged,
+ this, &KItemListHeader::leadingPaddingChanged);
}
*/
qreal preferredColumnWidth(const QByteArray& role) const;
+ /**
+ * Sets the width of the column *before* the first column.
+ * This is intended to facilitate an empty region for deselection in the main viewport.
+ */
+ void setLeadingPadding(qreal width);
+ qreal leadingPadding() const;
+
Q_SIGNALS:
+ void leadingPaddingChanged(qreal width);
+
/**
* Is emitted if the width of a column has been adjusted by the user with the mouse
* (no signal is emitted if KItemListHeader::setColumnWidth() is invoked).
const KItemListWidget* widget = it.value();
const QPointF mappedPos = widget->mapFromItem(this, pos);
- if (widget->contains(mappedPos)) {
+ if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
return it.key();
}
}
return m_supportsItemExpanding;
}
+void KItemListView::setHighlightEntireRow(bool highlightEntireRow)
+{
+ if (m_highlightEntireRow != highlightEntireRow) {
+ m_highlightEntireRow = highlightEntireRow;
+ onHighlightEntireRowChanged(highlightEntireRow);
+ }
+}
+
+bool KItemListView::highlightEntireRow() const
+{
+ return m_highlightEntireRow;
+}
+
+void KItemListView::setAlternateBackgrounds(bool alternate)
+{
+ if (m_alternateBackgrounds != alternate) {
+ m_alternateBackgrounds = alternate;
+ updateAlternateBackgrounds();
+ }
+}
+
+bool KItemListView::alternateBackgrounds() const
+{
+ return m_alternateBackgrounds;
+}
+
QRectF KItemListView::itemRect(int index) const
{
return m_layouter->itemRect(index);
connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged,
this, &KItemListView::slotHeaderColumnWidthChanged);
+ connect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged,
+ this, &KItemListView::slotLeadingPaddingChanged);
connect(m_headerWidget, &KItemListHeaderWidget::columnMoved,
this, &KItemListView::slotHeaderColumnMoved);
connect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged,
} else if (!visible && m_headerWidget->isVisible()) {
disconnect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged,
this, &KItemListView::slotHeaderColumnWidthChanged);
+ disconnect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged,
+ this, &KItemListView::slotLeadingPaddingChanged);
disconnect(m_headerWidget, &KItemListHeaderWidget::columnMoved,
this, &KItemListView::slotHeaderColumnMoved);
disconnect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged,
size,
m_layouter->itemMargin());
- const bool alternateBackgroundsChanged = (m_visibleRoles.count() > 1) &&
+ const bool alternateBackgroundsChanged = m_alternateBackgrounds &&
(( m_itemSize.isEmpty() && !size.isEmpty()) ||
(!m_itemSize.isEmpty() && size.isEmpty()));
Q_UNUSED(previous)
}
+void KItemListView::onHighlightEntireRowChanged(bool highlightEntireRow)
+{
+ Q_UNUSED(highlightEntireRow)
+}
+
void KItemListView::onSupportsItemExpandingChanged(bool supportsExpanding)
{
Q_UNUSED(supportsExpanding)
doLayout(NoAnimation);
}
+void KItemListView::slotLeadingPaddingChanged(qreal width)
+{
+ Q_UNUSED(width)
+ if (m_headerWidget->automaticColumnResizing()) {
+ applyAutomaticColumnWidths();
+ }
+ applyColumnWidthsFromHeader();
+ doLayout(NoAnimation);
+}
+
void KItemListView::slotHeaderColumnMoved(const QByteArray& role,
int currentIndex,
int previousIndex)
bool KItemListView::useAlternateBackgrounds() const
{
- return m_itemSize.isEmpty() && m_visibleRoles.count() > 1;
+ return m_alternateBackgrounds && m_itemSize.isEmpty();
}
QHash<QByteArray, qreal> KItemListView::preferredColumnWidths(const KItemRangeList& itemRanges) const
void KItemListView::applyColumnWidthsFromHeader()
{
// Apply the new size to the layouter
- const qreal requiredWidth = columnWidthsSum();
+ const qreal requiredWidth = columnWidthsSum() + m_headerWidget->leadingPadding();
const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth),
m_itemSize.height());
m_layouter->setItemSize(dynamicItemSize);
for (const QByteArray& role : qAsConst(m_visibleRoles)) {
widget->setColumnWidth(role, m_headerWidget->columnWidth(role));
}
+ widget->setLeadingPadding(m_headerWidget->leadingPadding());
}
void KItemListView::updatePreferredColumnWidths(const KItemRangeList& itemRanges)
qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole);
QSizeF dynamicItemSize = m_itemSize;
- qreal requiredWidth = columnWidthsSum();
+ qreal requiredWidth = columnWidthsSum() + m_headerWidget->leadingPadding();
const qreal availableWidth = size().width();
if (requiredWidth < availableWidth) {
// Stretch the first column to use the whole remaining width
void setSupportsItemExpanding(bool supportsExpanding);
bool supportsItemExpanding() const;
+ void setHighlightEntireRow(bool highlightEntireRow);
+ bool highlightEntireRow() const;
+
+ void setAlternateBackgrounds(bool alternate);
+ bool alternateBackgrounds() const;
+
/**
* @return The rectangle of the item relative to the top/left of
* the currently visible area (see KItemListView::offset()).
virtual void onScrollOffsetChanged(qreal current, qreal previous);
virtual void onVisibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous);
virtual void onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous);
+ virtual void onHighlightEntireRowChanged(bool highlightEntireRow);
virtual void onSupportsItemExpandingChanged(bool supportsExpanding);
virtual void onTransactionBegin();
qreal currentWidth,
qreal previousWidth);
+ void slotLeadingPaddingChanged(qreal width);
+
/**
* Is invoked if a column has been moved by the user. Applies
* the moved role to the view.
private:
bool m_enabledSelectionToggles;
bool m_grouped;
+ bool m_highlightEntireRow;
+ bool m_alternateBackgrounds;
bool m_supportsItemExpanding;
bool m_editingRole;
int m_activeTransactions; // Counter for beginTransaction()/endTransaction()
m_selected(false),
m_current(false),
m_hovered(false),
+ m_expansionAreaHovered(false),
m_alternateBackground(false),
m_enabledSelectionToggle(false),
m_data(),
return m_columnWidths.value(role);
}
+qreal KItemListWidget::leadingPadding() const {
+ return m_leadingPadding;
+}
+
+void KItemListWidget::setLeadingPadding(qreal width) {
+ if (m_leadingPadding != width){
+ m_leadingPadding = width;
+ leadingPaddingChanged(width);
+ update();
+ }
+}
+
void KItemListWidget::setStyleOption(const KItemListStyleOption& option)
{
if (m_styleOption == option) {
return m_hovered;
}
+void KItemListWidget::setExpansionAreaHovered(bool hovered)
+{
+ if (hovered == m_expansionAreaHovered) {
+ return;
+ }
+ m_expansionAreaHovered = hovered;
+ update();
+}
+
+bool KItemListWidget::expansionAreaHovered() const
+{
+ return m_expansionAreaHovered;
+}
+
void KItemListWidget::setHoverPosition(const QPointF& pos)
{
if (m_selectionToggle) {
Q_UNUSED(previous)
}
+void KItemListWidget::leadingPaddingChanged(qreal width)
+{
+ Q_UNUSED(width)
+}
+
void KItemListWidget::styleOptionChanged(const KItemListStyleOption& current,
const KItemListStyleOption& previous)
{
void setColumnWidth(const QByteArray& role, qreal width);
qreal columnWidth(const QByteArray& role) const;
+ void setLeadingPadding(qreal width);
+ qreal leadingPadding() const;
+
void setStyleOption(const KItemListStyleOption& option);
const KItemListStyleOption& styleOption() const;
void setHovered(bool hovered);
bool isHovered() const;
+ void setExpansionAreaHovered(bool hover);
+ bool expansionAreaHovered() const;
+
void setHoverPosition(const QPointF& pos);
void setAlternateBackground(bool enable);
virtual void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>());
virtual void visibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous);
virtual void columnWidthChanged(const QByteArray& role, qreal current, qreal previous);
+ virtual void leadingPaddingChanged(qreal width);
virtual void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous);
virtual void currentChanged(bool current);
virtual void selectedChanged(bool selected);
virtual void siblingsInformationChanged(const QBitArray& current, const QBitArray& previous);
virtual void editedRoleChanged(const QByteArray& current, const QByteArray& previous);
void resizeEvent(QGraphicsSceneResizeEvent* event) override;
+ void clearHoverCache();
/**
* Called when the user starts hovering this item.
private:
void initializeSelectionToggle();
void setHoverOpacity(qreal opacity);
- void clearHoverCache();
void drawItemStyleOption(QPainter* painter, QWidget* widget, QStyle::State styleState);
private:
bool m_selected;
bool m_current;
bool m_hovered;
+ bool m_expansionAreaHovered;
bool m_alternateBackground;
bool m_enabledSelectionToggle;
QHash<QByteArray, QVariant> m_data;
QList<QByteArray> m_visibleRoles;
QHash<QByteArray, qreal> m_columnWidths;
+ qreal m_leadingPadding;
KItemListStyleOption m_styleOption;
QBitArray m_siblingsInfo;
setAcceptDrops(true);
setScrollOrientation(Qt::Vertical);
setVisibleRoles({"text"});
+ setAlternateBackgrounds(true);
}
KStandardItemListView::~KStandardItemListView()
const ItemLayout previous = m_itemLayout;
m_itemLayout = layout;
+ // keep the leading padding option unchanged here
+ setHighlightEntireRow(layout == DetailsLayout);
setSupportsItemExpanding(itemLayoutSupportsItemExpanding(layout));
setScrollOrientation(layout == CompactLayout ? Qt::Horizontal : Qt::Vertical);
default: Q_ASSERT(false); break;
}
+ standardItemListWidget->setHighlightEntireRow(highlightEntireRow());
standardItemListWidget->setSupportsItemExpanding(supportsItemExpanding());
}
m_pixmapPos(),
m_pixmap(),
m_scaledPixmapSize(),
+ m_columnWidthSum(),
m_iconRect(),
m_hoverPixmap(),
m_textRect(),
return m_layout;
}
+void KStandardItemListWidget::setHighlightEntireRow(bool highlightEntireRow) {
+ if (m_highlightEntireRow != highlightEntireRow) {
+ m_highlightEntireRow = highlightEntireRow;
+ m_dirtyLayout = true;
+ update();
+ }
+}
+
+bool KStandardItemListWidget::highlightEntireRow() const {
+ return m_highlightEntireRow;
+}
+
void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding)
{
if (m_supportsItemExpanding != supportsItemExpanding) {
case DetailsLayout: {
const int padding = styleOption().padding;
QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding);
- return adjustedIconRect | m_textRect;
+ QRectF result = adjustedIconRect | m_textRect;
+ if (m_highlightEntireRow) {
+ result.setRight(m_columnWidthSum + leadingPadding());
+ }
+ return result;
}
default:
m_dirtyLayout = true;
}
+void KStandardItemListWidget::leadingPaddingChanged(qreal padding) {
+ Q_UNUSED(padding)
+ m_dirtyLayout = true;
+}
+
void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption& current,
const KItemListStyleOption& previous)
{
m_isHidden = isHidden();
m_customizedFont = customizedFont(styleOption().font);
m_customizedFontMetrics = QFontMetrics(m_customizedFont);
+ m_columnWidthSum = std::accumulate(m_sortedVisibleRoles.begin(), m_sortedVisibleRoles.end(),
+ qreal(), [this](qreal sum, const auto &role){ return sum + columnWidth(role); });
updateExpansionArea();
updateTextsCache();
updatePixmapCache();
+ clearHoverCache();
m_dirtyLayout = false;
m_dirtyContent = false;
const qreal inc = (widgetHeight - option.iconSize) / 2;
const qreal x = expandedParentsCount * widgetHeight + inc;
const qreal y = inc;
- m_expansionArea = QRectF(x, y, option.iconSize, option.iconSize);
+ const qreal xPadding = m_highlightEntireRow ? leadingPadding() : 0;
+ m_expansionArea = QRectF(xPadding + x, y, option.iconSize, option.iconSize);
return;
}
}
if (m_supportsItemExpanding) {
firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2;
} else {
- firstColumnInc += option.padding;
+ firstColumnInc += option.padding + leadingPadding();
}
qreal x = firstColumnInc;
const bool isTextRole = (role == "text");
if (isTextRole) {
- availableTextWidth -= firstColumnInc;
+ availableTextWidth -= firstColumnInc - leadingPadding();
}
if (requiredWidth > availableTextWidth) {
// The column after the name should always be aligned on the same x-position independent
// from the expansion-level shown in the name column
- x -= firstColumnInc;
+ x -= firstColumnInc - leadingPadding();
} else if (isRoleRightAligned(role)) {
textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc;
}
QColor c1;
if (m_customTextColor.isValid()) {
c1 = m_customTextColor;
- } else if (isSelected() && m_layout != DetailsLayout) {
- c1 = styleOption().palette.highlightedText().color();
+ } else if (isSelected()) {
+ // The detail text colour needs to match the main text (HighlightedText) for the same level
+ // of readability. We short circuit early here to avoid interpolating with another colour.
+ m_additionalInfoTextColor = styleOption().palette.color(QPalette::HighlightedText);
+ return;
} else {
c1 = styleOption().palette.text().color();
}
const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2;
QRect siblingRect(x, 0, siblingSize, siblingSize);
- QStyleOption option;
- option.palette.setColor(QPalette::Text, option.palette.color(normalTextColorRole()));
bool isItemSibling = true;
const QBitArray siblings = siblingsInformation();
+ QStyleOption option;
+ const auto normalColor = option.palette.color(normalTextColorRole());
+ const auto highlightColor = option.palette.color(expansionAreaHovered() ? QPalette::Highlight : normalTextColorRole());
for (int i = siblings.count() - 1; i >= 0; --i) {
option.rect = siblingRect;
option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None;
-
if (isItemSibling) {
option.state |= QStyle::State_Item;
if (m_isExpandable) {
if (data().value("isExpanded").toBool()) {
option.state |= QStyle::State_Open;
}
+ option.palette.setColor(QPalette::Text, highlightColor);
isItemSibling = false;
+ } else {
+ option.palette.setColor(QPalette::Text, normalColor);
}
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter);
void setLayout(Layout layout);
Layout layout() const;
+ void setHighlightEntireRow(bool highlightEntireRow);
+ bool highlightEntireRow() const;
+
void setSupportsItemExpanding(bool supportsItemExpanding);
bool supportsItemExpanding() const;
void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>()) override;
void visibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous) override;
void columnWidthChanged(const QByteArray& role, qreal current, qreal previous) override;
+ void leadingPaddingChanged(qreal width) override;
void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) override;
void hoveredChanged(bool hovered) override;
void selectedChanged(bool selected) override;
QFont m_customizedFont;
QFontMetrics m_customizedFontMetrics;
bool m_isExpandable;
+ bool m_highlightEntireRow;
bool m_supportsItemExpanding;
bool m_dirtyLayout;
QPixmap m_pixmap;
QSize m_scaledPixmapSize; //Size of the pixmap in device independent pixels
+ qreal m_columnWidthSum;
QRectF m_iconRect; // Cache for KItemListWidget::iconRect()
QPixmap m_hoverPixmap; // Cache for modified m_pixmap when hovering the item
m_automaticColumnResizing(true),
m_model(nullptr),
m_offset(0),
+ m_leadingPadding(0),
m_columns(),
m_columnWidths(),
m_preferredColumnWidths(),
return m_offset;
}
+void KItemListHeaderWidget::setLeadingPadding(qreal width)
+{
+ if (m_leadingPadding != width) {
+ m_leadingPadding = width;
+ leadingPaddingChanged(width);
+ update();
+ }
+}
+
+qreal KItemListHeaderWidget::leadingPadding() const
+{
+ return m_leadingPadding;
+}
+
qreal KItemListHeaderWidget::minimumColumnWidth() const
{
QFontMetricsF fontMetrics(font());
painter->setFont(font());
painter->setPen(palette().text().color());
- qreal x = -m_offset;
+ qreal x = -m_offset + m_leadingPadding;
int orderIndex = 0;
for (const QByteArray& role : qAsConst(m_columns)) {
const qreal roleWidth = m_columnWidths.value(role);
void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
if (event->button() & Qt::LeftButton) {
- updatePressedRoleIndex(event->pos());
m_pressedMousePos = event->pos();
- m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ?
- ResizeRoleOperation : NoRoleOperation;
+ if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) {
+ m_roleOperation = ResizeLeadingColumnOperation;
+ } else {
+ updatePressedRoleIndex(event->pos());
+ m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ?
+ ResizeRoleOperation : NoRoleOperation;
+ }
event->accept();
} else {
event->ignore();
} else {
m_movingRole.pixmap = createRolePixmap(roleIndex);
- qreal roleX = -m_offset;
+ qreal roleX = -m_offset + m_leadingPadding;
for (int i = 0; i < roleIndex; ++i) {
const QByteArray role = m_columns[i];
roleX += m_columnWidths.value(role);
break;
}
+ case ResizeLeadingColumnOperation: {
+ qreal currentWidth = m_leadingPadding;
+ currentWidth += event->pos().x() - event->lastPos().x();
+ currentWidth = qMax(0.0, currentWidth);
+
+ m_leadingPadding = currentWidth;
+
+ update();
+
+ Q_EMIT leadingPaddingChanged(currentWidth);
+
+ break;
+ }
+
case MoveRoleOperation: {
// TODO: It should be configurable whether moving the first role is allowed.
// In the context of Dolphin this is not required, however this should be
const QPointF& pos = event->pos();
updateHoveredRoleIndex(pos);
- if (m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) {
+ if ((m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) ||
+ isAbovePaddingGrip(pos, PaddingGrip::Leading) ||
+ isAbovePaddingGrip(pos, PaddingGrip::Trailing)) {
setCursor(Qt::SplitHCursor);
} else {
unsetCursor();
QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
}
option.rect = rect.toRect();
+ option.orientation = Qt::Horizontal;
+ option.selectedPosition = QStyleOptionHeader::NotAdjacent;
+ option.text = m_model->roleDescription(role);
- bool paintBackgroundForEmptyArea = false;
+ // First we paint any potential empty (padding) space on left and/or right of this role's column.
+ const auto paintPadding = [&](int section, const QRectF &rect, const QStyleOptionHeader::SectionPosition &pos){
+ QStyleOptionHeader padding;
+ padding.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
+ padding.section = section;
+ padding.sortIndicator = QStyleOptionHeader::None;
+ padding.rect = rect.toRect();
+ padding.position = pos;
+ padding.text = QString();
+ style()->drawControl(QStyle::CE_Header, &padding, painter, widget);
+ };
if (m_columns.count() == 1) {
- option.position = QStyleOptionHeader::OnlyOneSection;
+ option.position = QStyleOptionHeader::Middle;
+ paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
+ paintPadding(1, QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
} else if (orderIndex == 0) {
- option.position = QStyleOptionHeader::Beginning;
+ // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
+ if (rect.left() > 0) {
+ option.position = QStyleOptionHeader::Middle;
+ paintPadding(0,QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
+ } else {
+ option.position = QStyleOptionHeader::Beginning;
+ }
} else if (orderIndex == m_columns.count() - 1) {
- // We are just painting the header for the last column. Check if there
- // is some empty space to the right which needs to be filled.
+ // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
if (rect.right() < size().width()) {
option.position = QStyleOptionHeader::Middle;
- paintBackgroundForEmptyArea = true;
+ paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
} else {
option.position = QStyleOptionHeader::End;
}
option.position = QStyleOptionHeader::Middle;
}
- option.orientation = Qt::Horizontal;
- option.selectedPosition = QStyleOptionHeader::NotAdjacent;
- option.text = m_model->roleDescription(role);
-
style()->drawControl(QStyle::CE_Header, &option, painter, widget);
-
- if (paintBackgroundForEmptyArea) {
- option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
- option.section = m_columns.count();
- option.sortIndicator = QStyleOptionHeader::None;
-
- qreal backgroundRectX = rect.x() + rect.width();
- QRectF backgroundRect(backgroundRectX, 0.0, size().width() - backgroundRectX, rect.height());
- option.rect = backgroundRect.toRect();
- option.position = QStyleOptionHeader::End;
- option.text = QString();
-
- style()->drawControl(QStyle::CE_Header, &option, painter, widget);
- }
}
void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos)
{
int index = -1;
- qreal x = -m_offset;
+ qreal x = -m_offset + m_leadingPadding;
for (const QByteArray& role : qAsConst(m_columns)) {
++index;
x += m_columnWidths.value(role);
bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const
{
- qreal x = -m_offset;
+ qreal x = -m_offset + m_leadingPadding;
for (int i = 0; i <= roleIndex; ++i) {
const QByteArray role = m_columns[i];
x += m_columnWidths.value(role);
return pos.x() >= (x - grip) && pos.x() <= x;
}
+bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const
+{
+ const qreal lx = -m_offset + m_leadingPadding;
+ const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
+
+ switch (paddingGrip) {
+ case Leading:
+ return pos.x() >= (lx - grip) && pos.x() <= lx;
+ case Trailing:
+ {
+ qreal rx = lx;
+ for (const QByteArray& role : qAsConst(m_columns)) {
+ rx += m_columnWidths.value(role);
+ }
+ return pos.x() >= (rx - grip) && pos.x() <= rx;
+ }
+ default:
+ return false;
+ }
+}
+
QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
{
const QByteArray role = m_columns[roleIndex];
const int movingRight = movingLeft + movingWidth - 1;
int targetIndex = 0;
- qreal targetLeft = -m_offset;
+ qreal targetLeft = -m_offset + m_leadingPadding;
while (targetIndex < m_columns.count()) {
const QByteArray role = m_columns[targetIndex];
const qreal targetWidth = m_columnWidths.value(role);
qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const
{
- qreal x = -m_offset;
+ qreal x = -m_offset + m_leadingPadding;
for (const QByteArray& visibleRole : qAsConst(m_columns)) {
if (visibleRole == role) {
return x;
void setOffset(qreal offset);
qreal offset() const;
+ void setLeadingPadding(qreal width);
+ qreal leadingPadding() const;
+
qreal minimumColumnWidth() const;
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override;
qreal currentWidth,
qreal previousWidth);
+ void leadingPaddingChanged(qreal width);
+
/**
* Is emitted if the user has released the mouse button after adjusting the
* width of a visible role.
void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
private:
+
+ enum PaddingGrip
+ {
+ Leading,
+ Trailing,
+ };
+
void paintRole(QPainter* painter,
const QByteArray& role,
const QRectF& rect,
void updateHoveredRoleIndex(const QPointF& pos);
int roleIndexAt(const QPointF& pos) const;
bool isAboveRoleGrip(const QPointF& pos, int roleIndex) const;
+ bool isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const;
/**
* Creates a pixmap of the role with the index \a roleIndex that is shown
{
NoRoleOperation,
ResizeRoleOperation,
+ ResizeLeadingColumnOperation,
MoveRoleOperation
};
bool m_automaticColumnResizing;
KItemModelBase* m_model;
qreal m_offset;
+ qreal m_leadingPadding;
QList<QByteArray> m_columns;
QHash<QByteArray, qreal> m_columnWidths;
QHash<QByteArray, qreal> m_preferredColumnWidths;
<label>Position of columns</label>
<default>0,1,2,3,4,5,6,7,8</default>
</entry>
+ <entry name="LeadingPadding" type="UInt">
+ <label>Leading Column Padding</label>
+ <default>20</default>
+ </entry>
<entry name="ExpandableFolders" type="Bool">
<label>Expandable folders</label>
<default>true</default>
beginTransaction();
setEnabledSelectionToggles(GeneralSettings::showSelectionToggle());
+ setHighlightEntireRow(DetailsModeSettings::leadingPadding());
setSupportsItemExpanding(itemLayoutSupportsItemExpanding(itemLayout()));
updateFont();
#include "dolphinview.h"
#include "dolphin_generalsettings.h"
+#include "dolphin_detailsmodesettings.h"
#include "dolphinitemlistview.h"
#include "dolphinnewfilemenuobserver.h"
#include "draganddrophelper.h"
this, &DolphinView::slotRoleEditingCanceled);
connect(m_view->header(), &KItemListHeader::columnWidthChangeFinished,
this, &DolphinView::slotHeaderColumnWidthChangeFinished);
+ connect(m_view->header(), &KItemListHeader::leadingPaddingChanged,
+ this, &DolphinView::slotLeadingPaddingWidthChanged);
KItemListSelectionManager* selectionManager = controller->selectionManager();
connect(selectionManager, &KItemListSelectionManager::selectionChanged,
QActionGroup* widthsGroup = new QActionGroup(menu);
const bool autoColumnWidths = props.headerColumnWidths().isEmpty();
+ QAction* toggleLeadingPaddingAction = menu->addAction(i18nc("@action:inmenu", "Leading Column Padding"));
+ toggleLeadingPaddingAction->setCheckable(true);
+ toggleLeadingPaddingAction->setChecked(view->header()->leadingPadding() > 0);
+
QAction* autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths"));
autoAdjustWidthsAction->setCheckable(true);
autoAdjustWidthsAction->setChecked(autoColumnWidths);
}
props.setHeaderColumnWidths(columnWidths);
header->setAutomaticColumnResizing(false);
+ } else if (action == toggleLeadingPaddingAction) {
+ header->setLeadingPadding(toggleLeadingPaddingAction->isChecked() ? 20 : 0);
} else {
// Show or hide the selected role
const QByteArray selectedRole = action->data().toByteArray();
props.setHeaderColumnWidths(columnWidths);
}
+void DolphinView::slotLeadingPaddingWidthChanged(qreal width)
+{
+ ViewProperties props(viewPropertiesUrl());
+ DetailsModeSettings::setLeadingPadding(int(width));
+ m_view->writeSettings();
+}
+
void DolphinView::slotItemHovered(int index)
{
const KFileItem item = m_model->fileItem(index);
} else {
header->setAutomaticColumnResizing(true);
}
+ header->setLeadingPadding(DetailsModeSettings::leadingPadding());
}
m_view->endTransaction();
void slotViewContextMenuRequested(const QPointF& pos);
void slotHeaderContextMenuRequested(const QPointF& pos);
void slotHeaderColumnWidthChangeFinished(const QByteArray& role, qreal current);
+ void slotLeadingPaddingWidthChanged(qreal width);
void slotItemHovered(int index);
void slotItemUnhovered(int index);
void slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event);