Skip to content

Commit

Permalink
headerview: Add a HeaderView class
Browse files Browse the repository at this point in the history
Reimplement QHeaderView.paintSection to avoid expensive calls to
QItemSelectionModel.isRowSelected.
  • Loading branch information
ales-erjavec committed Jan 8, 2021
1 parent e5d5fa0 commit e703c52
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 0 deletions.
10 changes: 10 additions & 0 deletions Orange/widgets/data/owtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from Orange.widgets import gui
from Orange.widgets.settings import Setting
from Orange.widgets.utils.headerview import HeaderView
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import OWWidget, Input, Output
from Orange.widgets.utils import datacaching
Expand Down Expand Up @@ -402,6 +403,15 @@ class DataTableView(TableView):
dataset: Table
input_slot: TableSlot

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
hheader = HeaderView(Qt.Horizontal, self, highlightSections=True)
vheader = HeaderView(Qt.Vertical, self, highlightSections=True)
hheader.setSectionsClickable(True)
vheader.setSectionsClickable(True)
self.setHorizontalHeader(hheader)
self.setVerticalHeader(vheader)


class OWDataTable(OWWidget):
name = "Data Table"
Expand Down
226 changes: 226 additions & 0 deletions Orange/widgets/utils/headerview.py
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)
104 changes: 104 additions & 0 deletions Orange/widgets/utils/tests/test_headerview.py
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)

0 comments on commit e703c52

Please sign in to comment.