diff options
| author | Felix Ernst <[email protected]> | 2024-12-29 11:42:22 +0000 |
|---|---|---|
| committer | Felix Ernst <[email protected]> | 2024-12-29 11:42:22 +0000 |
| commit | 95542a389112491abf3a31c338e7d78f7785f48e (patch) | |
| tree | 1a4ece8deef6626c4649538fbf22acdee43114cb /src/kitemviews/private/kitemlistheaderwidget.cpp | |
| parent | 3696213ccbbe27e9ef3fc85eb97dd32fd669066f (diff) | |
Mirror details view mode for right-to-left languages
This commit implements mirroring of the details view mode for right-to-
left languages. This is the last of the Dolphin view modes which did
not adapt to right-to-left languages correctly.
Implementation-wise this is mostly about adapting the math so all the
information is placed correctly no matter the view mode or layout
direction. While most of the view actually changes the painting code
for right-to-left languages, for the column header I decided to keep
the logic left-to-right and instead reverse the order of the role
columns.
To implement this mirroring I needed to rework quite a bit of logic, so
I used the opportunity to fix some bugs/behaviur quirks:
- Left and right padding is now saved and restored separately instead
of only saving the left padding
- Changing the right padding no longer disables "automatic column
resizing".
- The grip handles for column resizing can now be grabbed when near the
grip handle instead of only allowing grabbing when slightly to the
left of the grip.
- Role column headers now only show a hover highlight effect when the
mouse cursor is actually above that role and not above the grip
handle or the padding.
- There is now a soft-boarder when shrinking the right padding so
shrinking the padding "below zero width" will no longer immediately
clear automatic resize behaviour. So now it is possible to simply
remove the right padding by resizing it to zero width.
BUG: 449211
BUG: 495942
# Acknowledgement
This work is part of a my project funded through the NGI0 Entrust Fund,
a fund established by NLnet with financial support from the European
Commission's Next Generation Internet programme, under the aegis of DG
Communications Networks, Content and Technology.
Diffstat (limited to 'src/kitemviews/private/kitemlistheaderwidget.cpp')
| -rw-r--r-- | src/kitemviews/private/kitemlistheaderwidget.cpp | 324 |
1 files changed, 181 insertions, 143 deletions
diff --git a/src/kitemviews/private/kitemlistheaderwidget.cpp b/src/kitemviews/private/kitemlistheaderwidget.cpp index 02a4f939d..3dc82ad6b 100644 --- a/src/kitemviews/private/kitemlistheaderwidget.cpp +++ b/src/kitemviews/private/kitemlistheaderwidget.cpp @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2011 Peter Penz <[email protected]> + * SPDX-FileCopyrightText: 2022, 2024 Felix Ernst <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -12,18 +13,44 @@ #include <QPainter> #include <QStyleOptionHeader> +namespace +{ +/** + * @returns a list which has a reversed order of elements compared to @a list. + */ +QList<QByteArray> reversed(const QList<QByteArray> list) +{ + QList<QByteArray> reversedList; + for (auto i = list.rbegin(); i != list.rend(); i++) { + reversedList.emplaceBack(*i); + } + return reversedList; +}; + +/** + * @returns the index of the column for the name/text of items. This depends on the layoutDirection() and column count of @a itemListHeaderWidget. + */ +int nameColumnIndex(const KItemListHeaderWidget *itemListHeaderWidget) +{ + if (itemListHeaderWidget->layoutDirection() == Qt::LeftToRight) { + return 0; + } + return itemListHeaderWidget->columns().count() - 1; +}; +} + KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget *parent) : QGraphicsWidget(parent) , m_automaticColumnResizing(true) , m_model(nullptr) , m_offset(0) - , m_sidePadding(0) + , m_leftPadding(0) + , m_rightPadding(0) , m_columns() , m_columnWidths() , m_preferredColumnWidths() , m_hoveredIndex(-1) , m_pressedRoleIndex(-1) - , m_roleOperation(NoRoleOperation) , m_pressedMousePos() , m_movingRole() { @@ -82,13 +109,13 @@ void KItemListHeaderWidget::setColumns(const QList<QByteArray> &roles) } } - m_columns = roles; + m_columns = layoutDirection() == Qt::LeftToRight ? roles : reversed(roles); update(); } QList<QByteArray> KItemListHeaderWidget::columns() const { - return m_columns; + return layoutDirection() == Qt::LeftToRight ? m_columns : reversed(m_columns); } void KItemListHeaderWidget::setColumnWidth(const QByteArray &role, qreal width) @@ -132,18 +159,35 @@ qreal KItemListHeaderWidget::offset() const return m_offset; } -void KItemListHeaderWidget::setSidePadding(qreal width) +void KItemListHeaderWidget::setSidePadding(qreal leftPaddingWidth, qreal rightPaddingWidth) { - if (m_sidePadding != width) { - m_sidePadding = width; - Q_EMIT sidePaddingChanged(width); - update(); + bool changed = false; + if (m_leftPadding != leftPaddingWidth) { + m_leftPadding = leftPaddingWidth; + changed = true; } + + if (m_rightPadding != rightPaddingWidth) { + m_rightPadding = rightPaddingWidth; + changed = true; + } + + if (!changed) { + return; + } + + Q_EMIT sidePaddingChanged(leftPaddingWidth, rightPaddingWidth); + update(); +} + +qreal KItemListHeaderWidget::leftPadding() const +{ + return m_leftPadding; } -qreal KItemListHeaderWidget::sidePadding() const +qreal KItemListHeaderWidget::rightPadding() const { - return m_sidePadding; + return m_rightPadding; } qreal KItemListHeaderWidget::minimumColumnWidth() const @@ -165,7 +209,7 @@ void KItemListHeaderWidget::paint(QPainter *painter, const QStyleOptionGraphicsI painter->setFont(font()); painter->setPen(palette().text().color()); - qreal x = -m_offset + m_sidePadding; + qreal x = -m_offset + m_leftPadding + unusedSpace(); int orderIndex = 0; for (const QByteArray &role : std::as_const(m_columns)) { const qreal roleWidth = m_columnWidths.value(role); @@ -176,7 +220,6 @@ void KItemListHeaderWidget::paint(QPainter *painter, const QStyleOptionGraphicsI } if (!m_movingRole.pixmap.isNull()) { - Q_ASSERT(m_roleOperation == MoveRoleOperation); painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap); } } @@ -185,11 +228,9 @@ void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_pressedMousePos = event->pos(); - if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) { - m_roleOperation = ResizePaddingColumnOperation; - } else { + m_pressedGrip = isAboveResizeGrip(m_pressedMousePos); + if (!m_pressedGrip) { updatePressedRoleIndex(event->pos()); - m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? ResizeRoleOperation : NoRoleOperation; } event->accept(); } else { @@ -201,12 +242,15 @@ void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { QGraphicsWidget::mouseReleaseEvent(event); - if (m_pressedRoleIndex == -1) { - return; - } - - switch (m_roleOperation) { - case NoRoleOperation: { + if (m_pressedGrip) { + // Emitting a column width change removes automatic column resizing, so we do not emit if only the padding is being changed. + // Eception: In mouseMoveEvent() we also resize the last column if the right padding is at zero but the user still quickly resizes beyond the screen + // boarder. Such a resize "of the right padding" is let through when automatic column resizing was disabled by that resize. + if (m_pressedGrip->roleToTheLeft != "leftPadding" && (m_pressedGrip->roleToTheRight != "rightPadding" || !m_automaticColumnResizing)) { + const qreal currentWidth = m_columnWidths.value(m_pressedGrip->roleToTheLeft); + Q_EMIT columnWidthChangeFinished(m_pressedGrip->roleToTheLeft, currentWidth); + } + } else if (m_pressedRoleIndex != -1 && m_movingRole.index == -1) { // Only a click has been done and no moving or resizing has been started const QByteArray sortRole = m_model->sortRole(); const int sortRoleIndex = m_columns.indexOf(sortRole); @@ -229,29 +273,15 @@ void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) Q_EMIT sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder); } } - break; } - case ResizeRoleOperation: { - const QByteArray pressedRole = m_columns[m_pressedRoleIndex]; - const qreal currentWidth = m_columnWidths.value(pressedRole); - Q_EMIT columnWidthChangeFinished(pressedRole, currentWidth); - break; - } - - case MoveRoleOperation: - m_movingRole.pixmap = QPixmap(); - m_movingRole.x = 0; - m_movingRole.xDec = 0; - m_movingRole.index = -1; - break; - - default: - break; - } + m_movingRole.pixmap = QPixmap(); + m_movingRole.x = 0; + m_movingRole.xDec = 0; + m_movingRole.index = -1; + m_pressedGrip = std::nullopt; m_pressedRoleIndex = -1; - m_roleOperation = NoRoleOperation; update(); QApplication::restoreOverrideCursor(); @@ -261,69 +291,51 @@ void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QGraphicsWidget::mouseMoveEvent(event); - switch (m_roleOperation) { - case NoRoleOperation: - if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { - // A role gets dragged by the user. Create a pixmap of the role that will get - // synchronized on each further mouse-move-event with the mouse-position. - m_roleOperation = MoveRoleOperation; - const int roleIndex = roleIndexAt(m_pressedMousePos); - m_movingRole.index = roleIndex; - if (roleIndex == 0) { - // 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 - // changed if KItemViews are used in a more generic way. - QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor)); - } else { - m_movingRole.pixmap = createRolePixmap(roleIndex); + if (m_pressedGrip) { + if (m_pressedGrip->roleToTheLeft == "leftPadding") { + qreal currentWidth = m_leftPadding; + currentWidth += event->pos().x() - event->lastPos().x(); + m_leftPadding = qMax(0.0, currentWidth); - qreal roleX = -m_offset + m_sidePadding; - for (int i = 0; i < roleIndex; ++i) { - const QByteArray role = m_columns[i]; - roleX += m_columnWidths.value(role); - } + update(); + Q_EMIT sidePaddingChanged(m_leftPadding, m_rightPadding); + return; + } + + if (m_pressedGrip->roleToTheRight == "rightPadding") { + qreal currentWidth = m_rightPadding; + currentWidth -= event->pos().x() - event->lastPos().x(); + m_rightPadding = qMax(0.0, currentWidth); - m_movingRole.xDec = event->pos().x() - roleX; - m_movingRole.x = roleX; - update(); + update(); + Q_EMIT sidePaddingChanged(m_leftPadding, m_rightPadding); + if (m_rightPadding > 0.0) { + return; + } + // Continue so resizing of the last column beyond the view width is possible. + if (currentWidth > -10) { + return; // Automatic column resizing is valuable, so we don't want to give it up just for a few pixels of extra width for the rightmost column. } + m_automaticColumnResizing = false; } - break; - - case ResizeRoleOperation: { - const QByteArray pressedRole = m_columns[m_pressedRoleIndex]; - qreal previousWidth = m_columnWidths.value(pressedRole); + qreal previousWidth = m_columnWidths.value(m_pressedGrip->roleToTheLeft); qreal currentWidth = previousWidth; currentWidth += event->pos().x() - event->lastPos().x(); currentWidth = qMax(minimumColumnWidth(), currentWidth); - m_columnWidths.insert(pressedRole, currentWidth); + m_columnWidths.insert(m_pressedGrip->roleToTheLeft, currentWidth); update(); - Q_EMIT columnWidthChanged(pressedRole, currentWidth, previousWidth); - break; - } - - case ResizePaddingColumnOperation: { - qreal currentWidth = m_sidePadding; - currentWidth += event->pos().x() - event->lastPos().x(); - currentWidth = qMax(0.0, currentWidth); - - m_sidePadding = currentWidth; - - update(); - - Q_EMIT sidePaddingChanged(currentWidth); - - break; + Q_EMIT columnWidthChanged(m_pressedGrip->roleToTheLeft, currentWidth, previousWidth); + return; } - case MoveRoleOperation: { + if (m_movingRole.index != -1) { // 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 // changed if KItemViews are used in a more generic way. - if (m_movingRole.index > 0) { + if (m_movingRole.index != nameColumnIndex(this)) { m_movingRole.x = event->pos().x() - m_movingRole.xDec; update(); @@ -332,16 +344,42 @@ void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) const QByteArray role = m_columns[m_movingRole.index]; const int previousIndex = m_movingRole.index; m_movingRole.index = targetIndex; - Q_EMIT columnMoved(role, targetIndex, previousIndex); + if (layoutDirection() == Qt::LeftToRight) { + Q_EMIT columnMoved(role, targetIndex, previousIndex); + } else { + Q_EMIT columnMoved(role, m_columns.count() - 1 - targetIndex, m_columns.count() - 1 - previousIndex); + } m_movingRole.xDec = event->pos().x() - roleXPosition(role); } } - break; + return; } - default: - break; + if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { + // A role gets dragged by the user. Create a pixmap of the role that will get + // synchronized on each further mouse-move-event with the mouse-position. + const int roleIndex = roleIndexAt(m_pressedMousePos); + m_movingRole.index = roleIndex; + if (roleIndex == nameColumnIndex(this)) { + // 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 + // changed if KItemViews are used in a more generic way. + QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor)); + return; + } + + m_movingRole.pixmap = createRolePixmap(roleIndex); + + qreal roleX = -m_offset + m_leftPadding + unusedSpace(); + for (int i = 0; i < roleIndex; ++i) { + const QByteArray role = m_columns[i]; + roleX += m_columnWidths.value(role); + } + + m_movingRole.xDec = event->pos().x() - roleX; + m_movingRole.x = roleX; + update(); } } @@ -349,17 +387,17 @@ void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *even { QGraphicsItem::mouseDoubleClickEvent(event); - const int roleIndex = roleIndexAt(event->pos()); - if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) { - const QByteArray role = m_columns.at(roleIndex); + const std::optional<Grip> doubleClickedGrip = isAboveResizeGrip(event->pos()); + if (!doubleClickedGrip || doubleClickedGrip->roleToTheLeft.isEmpty()) { + return; + } - qreal previousWidth = columnWidth(role); - setColumnWidth(role, preferredColumnWidth(role)); - qreal currentWidth = columnWidth(role); + qreal previousWidth = columnWidth(doubleClickedGrip->roleToTheLeft); + setColumnWidth(doubleClickedGrip->roleToTheLeft, preferredColumnWidth(doubleClickedGrip->roleToTheLeft)); + qreal currentWidth = columnWidth(doubleClickedGrip->roleToTheLeft); - Q_EMIT columnWidthChanged(role, currentWidth, previousWidth); - Q_EMIT columnWidthChangeFinished(role, currentWidth); - } + Q_EMIT columnWidthChanged(doubleClickedGrip->roleToTheLeft, currentWidth, previousWidth); + Q_EMIT columnWidthChangeFinished(doubleClickedGrip->roleToTheLeft, currentWidth); } void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent *event) @@ -384,8 +422,7 @@ void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent *event) const QPointF &pos = event->pos(); updateHoveredIndex(pos); - if ((m_hoveredIndex >= 0 && isAboveRoleGrip(pos, m_hoveredIndex)) || isAbovePaddingGrip(pos, PaddingGrip::Leading) - || isAbovePaddingGrip(pos, PaddingGrip::Trailing)) { + if (isAboveResizeGrip(pos)) { setCursor(Qt::SplitHCursor); } else { unsetCursor(); @@ -408,14 +445,9 @@ void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::Sort void KItemListHeaderWidget::paintRole(QPainter *painter, const QByteArray &role, const QRectF &rect, int orderIndex, QWidget *widget) const { - const auto direction = widget ? widget->layoutDirection() : qApp->layoutDirection(); - // The following code is based on the code from QHeaderView::paintSection(). // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies). QStyleOptionHeader option; - option.direction = direction; - option.textAlignment = direction == Qt::LeftToRight ? Qt::AlignLeft : Qt::AlignRight; - option.section = orderIndex; option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; if (isEnabled()) { @@ -453,7 +485,7 @@ void KItemListHeaderWidget::paintRole(QPainter *painter, const QByteArray &role, if (m_columns.count() == 1) { 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); + paintPadding(1, QRectF(rect.right(), 0.0, size().width() - rect.right(), rect.height()), QStyleOptionHeader::End); } else if (orderIndex == 0) { // 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) { @@ -466,7 +498,7 @@ void KItemListHeaderWidget::paintRole(QPainter *painter, const QByteArray &role, // 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; - paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End); + paintPadding(m_columns.count(), QRectF(rect.right(), 0.0, size().width() - rect.right(), rect.height()), QStyleOptionHeader::End); } else { option.position = QStyleOptionHeader::End; } @@ -488,7 +520,7 @@ void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF &pos) void KItemListHeaderWidget::updateHoveredIndex(const QPointF &pos) { - const int hoverIndex = roleIndexAt(pos); + const int hoverIndex = isAboveResizeGrip(pos) ? -1 : roleIndexAt(pos); if (m_hoveredIndex != hoverIndex) { if (m_hoveredIndex != -1) { @@ -504,50 +536,43 @@ void KItemListHeaderWidget::updateHoveredIndex(const QPointF &pos) int KItemListHeaderWidget::roleIndexAt(const QPointF &pos) const { - int index = -1; + qreal x = -m_offset + m_leftPadding + unusedSpace(); + if (pos.x() < x) { + return -1; + } - qreal x = -m_offset + m_sidePadding; + int index = -1; for (const QByteArray &role : std::as_const(m_columns)) { ++index; x += m_columnWidths.value(role); if (pos.x() <= x) { - break; + return index; } } - return index; + return -1; } -bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF &pos, int roleIndex) const +std::optional<const KItemListHeaderWidget::Grip> KItemListHeaderWidget::isAboveResizeGrip(const QPointF &position) const { - qreal x = -m_offset + m_sidePadding; - for (int i = 0; i <= roleIndex; ++i) { - const QByteArray role = m_columns[i]; - x += m_columnWidths.value(role); - } - - const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin); - return pos.x() >= (x - grip) && pos.x() <= x; -} + qreal x = -m_offset + m_leftPadding + unusedSpace(); + const int gripWidthTolerance = style()->pixelMetric(QStyle::PM_HeaderGripMargin); -bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF &pos, PaddingGrip paddingGrip) const -{ - const qreal lx = -m_offset + m_sidePadding; - const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin); + if (x - gripWidthTolerance < position.x() && position.x() < x + gripWidthTolerance) { + return std::optional{Grip{"leftPadding", m_columns[0]}}; + } - switch (paddingGrip) { - case Leading: - return pos.x() >= (lx - grip) && pos.x() <= lx; - case Trailing: { - qreal rx = lx; - for (const QByteArray &role : std::as_const(m_columns)) { - rx += m_columnWidths.value(role); + for (int i = 0; i < m_columns.count(); ++i) { + const QByteArray role = m_columns[i]; + x += m_columnWidths.value(role); + if (x - gripWidthTolerance < position.x() && position.x() < x + gripWidthTolerance) { + if (i + 1 < m_columns.count()) { + return std::optional{Grip{m_columns[i], m_columns[i + 1]}}; + } + return std::optional{Grip{m_columns[i], "rightPadding"}}; } - return pos.x() >= (rx - grip) && pos.x() <= rx; - } - default: - return false; } + return std::nullopt; } QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const @@ -581,7 +606,7 @@ int KItemListHeaderWidget::targetOfMovingRole() const const int movingRight = movingLeft + movingWidth - 1; int targetIndex = 0; - qreal targetLeft = -m_offset + m_sidePadding; + qreal targetLeft = -m_offset + m_leftPadding + unusedSpace(); while (targetIndex < m_columns.count()) { const QByteArray role = m_columns[targetIndex]; const qreal targetWidth = m_columnWidths.value(role); @@ -603,7 +628,7 @@ int KItemListHeaderWidget::targetOfMovingRole() const qreal KItemListHeaderWidget::roleXPosition(const QByteArray &role) const { - qreal x = -m_offset + m_sidePadding; + qreal x = -m_offset + m_leftPadding + unusedSpace(); for (const QByteArray &visibleRole : std::as_const(m_columns)) { if (visibleRole == role) { return x; @@ -615,4 +640,17 @@ qreal KItemListHeaderWidget::roleXPosition(const QByteArray &role) const return -1; } +qreal KItemListHeaderWidget::unusedSpace() const +{ + if (layoutDirection() == Qt::LeftToRight) { + return 0; + } + int unusedSpace = size().width() - m_leftPadding - m_rightPadding; + for (int i = 0; i < m_columns.count(); ++i) { + const QByteArray role = m_columns[i]; + unusedSpace -= m_columnWidths.value(role); + } + return qMax(unusedSpace, 0); +} + #include "moc_kitemlistheaderwidget.cpp" |
