-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reimplement QHeaderView.paintSection to avoid expensive calls to QItemSelectionModel.isRowSelected.
- Loading branch information
1 parent
e5d5fa0
commit e703c52
Showing
3 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
from AnyQt.QtCore import Qt, QRect | ||
from AnyQt.QtGui import QBrush, QIcon, QCursor, QPalette, QPainter | ||
from AnyQt.QtWidgets import ( | ||
QHeaderView, QStyleOptionHeader, QStyle, QApplication | ||
) | ||
|
||
|
||
class HeaderView(QHeaderView): | ||
""" | ||
A QHeaderView reimplementing `paintSection` to better deal with | ||
selections in large models. | ||
In particular: | ||
* `isColumnSelected`/`isRowSelected` are never queried, only | ||
`rowIntersectsSelection`/`columnIntersectsSelection` are used. | ||
* when `highlightSections` is not enabled the selection model is not | ||
queried at all. | ||
""" | ||
def __init__(self, *args, **kwargs): | ||
self.__pressed = -1 # Tracking the pressed section index | ||
super().__init__(*args, **kwargs) | ||
|
||
def set_pressed(index): | ||
self.__pressed = index | ||
self.sectionPressed.connect(set_pressed) | ||
self.sectionEntered.connect(set_pressed) | ||
# Workaround for QTBUG-89910 | ||
self.setFont(QApplication.font("QHeaderView")) | ||
|
||
def mouseReleaseEvent(self, event): | ||
super().mouseReleaseEvent(event) | ||
self.__pressed = -1 | ||
|
||
def __sectionIntersectsSelection(self, logicalIndex): | ||
selmodel = self.selectionModel() | ||
if selmodel is None: | ||
return False # pragma: no cover | ||
root = self.rootIndex() | ||
if self.orientation() == Qt.Horizontal: | ||
return selmodel.columnIntersectsSelection(logicalIndex, root) | ||
else: | ||
return selmodel.rowIntersectsSelection(logicalIndex, root) | ||
|
||
def __isFirstVisibleSection(self, visualIndex): | ||
log = self.logicalIndex(visualIndex) | ||
if log != -1: | ||
return (self.sectionPosition(log) == 0 and | ||
self.sectionSize(log) > 0) | ||
else: | ||
return False # pragma: no cover | ||
|
||
def __isLastVisibleSection(self, visualIndex): | ||
log = self.logicalIndex(visualIndex) | ||
if log != -1: | ||
pos = self.sectionPosition(log) | ||
size = self.sectionSize(log) | ||
return size > 0 and pos + size == self.length() | ||
else: | ||
return False # pragma: no cover | ||
|
||
def initStyleOptionForIndex( | ||
self, option: QStyleOptionHeader, logicalIndex: int | ||
) -> None: | ||
""" | ||
Similar to initStyleOptionForIndex in Qt 6.0 with the difference that | ||
`isSectionSelected` is not used, only `sectionIntersectsSelection` | ||
is used (isSectionSelected will scan the entire model column/row | ||
when the whole column/row is selected). | ||
""" | ||
hover = self.logicalIndexAt(self.mapFromGlobal(QCursor.pos())) | ||
pressed = self.__pressed | ||
|
||
if self.highlightSections(): | ||
is_selected = self.__sectionIntersectsSelection | ||
else: | ||
is_selected = lambda _: False | ||
|
||
state = QStyle.State_None | ||
if self.isEnabled(): | ||
state |= QStyle.State_Enabled | ||
if self.window().isActiveWindow(): | ||
state |= QStyle.State_Active | ||
if self.sectionsClickable(): | ||
if logicalIndex == hover: | ||
state |= QStyle.State_MouseOver | ||
if logicalIndex == pressed: | ||
state |= QStyle.State_Sunken | ||
if self.highlightSections(): | ||
if is_selected(logicalIndex): | ||
state |= QStyle.State_On | ||
if self.isSortIndicatorShown() and \ | ||
self.sortIndicatorSection() == logicalIndex: | ||
option.sortIndicator = ( | ||
QStyleOptionHeader.SortDown | ||
if self.sortIndicatorOrder() == Qt.AscendingOrder | ||
else QStyleOptionHeader.SortUp | ||
) | ||
|
||
style = self.style() | ||
model = self.model() | ||
orientation = self.orientation() | ||
textAlignment = model.headerData(logicalIndex, self.orientation(), | ||
Qt.TextAlignmentRole) | ||
defaultAlignment = self.defaultAlignment() | ||
textAlignment = (textAlignment if isinstance(textAlignment, int) | ||
else defaultAlignment) | ||
|
||
option.section = logicalIndex | ||
option.state |= state | ||
option.textAlignment = Qt.Alignment(textAlignment) | ||
|
||
option.iconAlignment = Qt.AlignVCenter | ||
text = model.headerData(logicalIndex, self.orientation(), | ||
Qt.DisplayRole) | ||
text = str(text) if text is not None else "" | ||
option.text = text | ||
|
||
icon = model.headerData( | ||
logicalIndex, self.orientation(), Qt.DecorationRole) | ||
try: | ||
option.icon = QIcon(icon) | ||
except (TypeError, ValueError): # pragma: no cover | ||
pass | ||
|
||
margin = 2 * style.pixelMetric(QStyle.PM_HeaderMargin, None, self) | ||
|
||
headerArrowAlignment = style.styleHint(QStyle.SH_Header_ArrowAlignment, | ||
None, self) | ||
isHeaderArrowOnTheSide = headerArrowAlignment & Qt.AlignVCenter | ||
if self.isSortIndicatorShown() and \ | ||
self.sortIndicatorSection() == logicalIndex \ | ||
and isHeaderArrowOnTheSide: | ||
margin += style.pixelMetric(QStyle.PM_HeaderMarkSize, None, self) | ||
|
||
if not option.icon.isNull(): | ||
margin += style.pixelMetric(QStyle.PM_SmallIconSize, None, self) | ||
margin += style.pixelMetric(QStyle.PM_HeaderMargin, None, self) | ||
|
||
if self.textElideMode() != Qt.ElideNone: | ||
elideMode = self.textElideMode() | ||
if hasattr(option, 'textElideMode'): # Qt 6.0 | ||
option.textElideMode = elideMode # pragma: no cover | ||
else: | ||
option.text = option.fontMetrics.elidedText( | ||
option.text, elideMode, option.rect.width() - margin) | ||
|
||
foregroundBrush = model.headerData(logicalIndex, orientation, | ||
Qt.ForegroundRole) | ||
try: | ||
foregroundBrush = QBrush(foregroundBrush) | ||
except (TypeError, ValueError): | ||
pass | ||
else: | ||
option.palette.setBrush(QPalette.ButtonText, foregroundBrush) | ||
|
||
backgroundBrush = model.headerData(logicalIndex, orientation, | ||
Qt.BackgroundRole) | ||
try: | ||
backgroundBrush = QBrush(backgroundBrush) | ||
except (TypeError, ValueError): | ||
pass | ||
else: | ||
option.palette.setBrush(QPalette.Button, backgroundBrush) | ||
option.palette.setBrush(QPalette.Window, backgroundBrush) | ||
|
||
# the section position | ||
visual = self.visualIndex(logicalIndex) | ||
assert visual != -1 | ||
first = self.__isFirstVisibleSection(visual) | ||
last = self.__isLastVisibleSection(visual) | ||
if first and last: | ||
option.position = QStyleOptionHeader.OnlyOneSection | ||
elif first: | ||
option.position = QStyleOptionHeader.Beginning | ||
elif last: | ||
option.position = QStyleOptionHeader.End | ||
else: | ||
option.position = QStyleOptionHeader.Middle | ||
option.orientation = orientation | ||
|
||
# the selected position (in QHeaderView this is always computed even if | ||
# highlightSections is False). | ||
if self.highlightSections(): | ||
previousSelected = is_selected(self.logicalIndex(visual - 1)) | ||
nextSelected = is_selected(self.logicalIndex(visual + 1)) | ||
else: | ||
previousSelected = nextSelected = False | ||
|
||
if previousSelected and nextSelected: | ||
option.selectedPosition = QStyleOptionHeader.NextAndPreviousAreSelected | ||
elif previousSelected: | ||
option.selectedPosition = QStyleOptionHeader.PreviousIsSelected | ||
elif nextSelected: | ||
option.selectedPosition = QStyleOptionHeader.NextIsSelected | ||
else: | ||
option.selectedPosition = QStyleOptionHeader.NotAdjacent | ||
|
||
def paintSection(self, painter, rect, logicalIndex): | ||
# type: (QPainter, QRect, int) -> None | ||
""" | ||
Reimplemented from `QHeaderView`. | ||
""" | ||
# What follows is similar to QHeaderView::paintSection@Qt 6.0 | ||
if not rect.isValid(): | ||
return # pragma: no cover | ||
oldBO = painter.brushOrigin() | ||
|
||
opt = QStyleOptionHeader() | ||
opt.rect = rect | ||
self.initStyleOption(opt) | ||
|
||
oBrushButton = opt.palette.brush(QPalette.Button) | ||
oBrushWindow = opt.palette.brush(QPalette.Window) | ||
|
||
self.initStyleOptionForIndex(opt, logicalIndex) | ||
opt.rect = rect | ||
|
||
nBrushButton = opt.palette.brush(QPalette.Button) | ||
nBrushWindow = opt.palette.brush(QPalette.Window) | ||
|
||
if oBrushButton != nBrushButton or oBrushWindow != nBrushWindow: | ||
painter.setBrushOrigin(opt.rect.topLeft()) | ||
# draw the section | ||
self.style().drawControl(QStyle.CE_Header, opt, painter, self) | ||
|
||
painter.setBrushOrigin(oldBO) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
from AnyQt.QtGui import QStandardItemModel, QIcon, QColor | ||
from AnyQt.QtCore import Qt, QItemSelectionModel, QPoint | ||
from AnyQt.QtWidgets import QStyleOptionHeader, QStyle | ||
from AnyQt.QtTest import QTest | ||
|
||
|
||
from Orange.widgets.tests.base import GuiTest | ||
from Orange.widgets.utils.headerview import HeaderView | ||
from Orange.widgets.utils.textimport import StampIconEngine | ||
|
||
|
||
class TestHeaderView(GuiTest): | ||
def test_header(self): | ||
model = QStandardItemModel() | ||
|
||
hheader = HeaderView(Qt.Horizontal) | ||
vheader = HeaderView(Qt.Vertical) | ||
hheader.setSortIndicatorShown(True) | ||
|
||
# paint with no model. | ||
vheader.grab() | ||
hheader.grab() | ||
|
||
hheader.setModel(model) | ||
vheader.setModel(model) | ||
|
||
hheader.adjustSize() | ||
vheader.adjustSize() | ||
# paint with an empty model | ||
vheader.grab() | ||
hheader.grab() | ||
|
||
model.setRowCount(1) | ||
model.setColumnCount(1) | ||
icon = QIcon(StampIconEngine("A", Qt.red)) | ||
model.setHeaderData(0, Qt.Horizontal, icon, Qt.DecorationRole) | ||
model.setHeaderData(0, Qt.Vertical, icon, Qt.DecorationRole) | ||
model.setHeaderData(0, Qt.Horizontal, QColor(Qt.blue), Qt.ForegroundRole) | ||
model.setHeaderData(0, Qt.Vertical, QColor(Qt.blue), Qt.ForegroundRole) | ||
model.setHeaderData(0, Qt.Horizontal, QColor(Qt.white), Qt.BackgroundRole) | ||
model.setHeaderData(0, Qt.Vertical, QColor(Qt.white), Qt.BackgroundRole) | ||
|
||
# paint with single col/row model | ||
vheader.grab() | ||
hheader.grab() | ||
|
||
model.setRowCount(3) | ||
model.setColumnCount(3) | ||
|
||
hheader.adjustSize() | ||
vheader.adjustSize() | ||
|
||
# paint with single col/row model | ||
vheader.grab() | ||
hheader.grab() | ||
|
||
hheader.setSortIndicator(0, Qt.AscendingOrder) | ||
vheader.setHighlightSections(True) | ||
hheader.setHighlightSections(True) | ||
|
||
vheader.grab() | ||
hheader.grab() | ||
|
||
vheader.setSectionsClickable(True) | ||
hheader.setSectionsClickable(True) | ||
|
||
vheader.grab() | ||
hheader.grab() | ||
|
||
vheader.setTextElideMode(Qt.ElideRight) | ||
hheader.setTextElideMode(Qt.ElideRight) | ||
|
||
selmodel = QItemSelectionModel(model, model) | ||
|
||
vheader.setSelectionModel(selmodel) | ||
hheader.setSelectionModel(selmodel) | ||
|
||
selmodel.select(model.index(1, 1), QItemSelectionModel.Rows | QItemSelectionModel.Select) | ||
selmodel.select(model.index(1, 1), QItemSelectionModel.Columns | QItemSelectionModel.Select) | ||
|
||
vheader.grab() | ||
vheader.grab() | ||
|
||
def test_header_view_clickable(self): | ||
model = QStandardItemModel() | ||
model.setColumnCount(3) | ||
header = HeaderView(Qt.Horizontal) | ||
header.setModel(model) | ||
header.setSectionsClickable(True) | ||
header.adjustSize() | ||
pos = header.sectionViewportPosition(0) | ||
size = header.sectionSize(0) | ||
# center of first section | ||
point = QPoint(pos + size // 2, header.viewport().height() / 2) | ||
QTest.mousePress(header.viewport(), Qt.LeftButton, Qt.NoModifier, point) | ||
|
||
opt = QStyleOptionHeader() | ||
header.initStyleOptionForIndex(opt, 0) | ||
self.assertTrue(opt.state & QStyle.State_Sunken) | ||
|
||
QTest.mouseRelease(header.viewport(), Qt.LeftButton, Qt.NoModifier, point) | ||
opt = QStyleOptionHeader() | ||
header.initStyleOptionForIndex(opt, 0) | ||
self.assertFalse(opt.state & QStyle.State_Sunken) |