Skip to content

Commit 4ee7165

Browse files
committed
textimport: Check indicators
1 parent 703caca commit 4ee7165

File tree

3 files changed

+174
-6
lines changed

3 files changed

+174
-6
lines changed

Orange/widgets/utils/headerview.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from AnyQt.QtCore import Qt, QRect
1+
from __future__ import annotations
2+
3+
from AnyQt.QtCore import Qt, QRect, QSize
24
from AnyQt.QtGui import QBrush, QIcon, QCursor, QPalette, QPainter, QMouseEvent
35
from AnyQt.QtWidgets import (
4-
QHeaderView, QStyleOptionHeader, QStyle, QApplication
6+
QHeaderView, QStyleOptionHeader, QStyle, QApplication, QStyleOptionViewItem
57
)
68

79

@@ -225,3 +227,139 @@ def paintSection(self, painter, rect, logicalIndex):
225227
self.style().drawControl(QStyle.CE_Header, opt, painter, self)
226228

227229
painter.setBrushOrigin(oldBO)
230+
231+
232+
class CheckableHeaderView(HeaderView):
233+
"""
234+
A HeaderView with checkable header items.
235+
236+
The header is checkable if the model defines a `Qt.CheckStateRole` value.
237+
"""
238+
__sectionPressed: int = -1
239+
240+
def paintSection(
241+
self, painter: QPainter, rect: QRect, logicalIndex: int
242+
) -> None:
243+
opt = QStyleOptionHeader()
244+
self.initStyleOption(opt)
245+
self.initStyleOptionForIndex(opt, logicalIndex)
246+
model = self.model()
247+
if model is None:
248+
return # pragma: no cover
249+
opt.rect = rect
250+
checkstate = self.sectionCheckState(logicalIndex)
251+
ischeckable = checkstate is not None
252+
style = self.style()
253+
# draw background
254+
style.drawControl(QStyle.CE_HeaderSection, opt, painter, self)
255+
text_rect = QRect(rect)
256+
optindicator = QStyleOptionViewItem()
257+
optindicator.initFrom(self)
258+
optindicator.font = self.font()
259+
optindicator.fontMetrics = opt.fontMetrics
260+
optindicator.features = QStyleOptionViewItem.HasCheckIndicator | QStyleOptionViewItem.HasDisplay
261+
optindicator.rect = opt.rect
262+
indicator_rect = style.subElementRect(
263+
QStyle.SE_ItemViewItemCheckIndicator, optindicator, self)
264+
text_rect.setLeft(indicator_rect.right() + 4)
265+
if ischeckable:
266+
optindicator.checkState = checkstate
267+
optindicator.state |= QStyle.State_On if checkstate == Qt.Checked else QStyle.State_Off
268+
optindicator.rect = indicator_rect
269+
style.drawPrimitive(QStyle.PE_IndicatorItemViewItemCheck, optindicator,
270+
painter, self)
271+
opt.rect = text_rect
272+
# draw section label
273+
style.drawControl(QStyle.CE_HeaderLabel, opt, painter, self)
274+
275+
def mousePressEvent(self, event: QMouseEvent) -> None:
276+
pos = event.pos()
277+
section = self.logicalIndexAt(pos)
278+
if section == -1 or not self.isSectionCheckable(section):
279+
return super().mousePressEvent(event)
280+
281+
if event.button() == Qt.LeftButton:
282+
opt = self.__viewItemOption(section)
283+
hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self)
284+
if hitrect.contains(pos):
285+
self.__sectionPressed = section
286+
event.accept()
287+
return
288+
return super().mousePressEvent(event)
289+
290+
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
291+
pos = event.pos()
292+
section = self.logicalIndexAt(pos)
293+
if section == -1 or not self.isSectionCheckable(section) \
294+
or self.__sectionPressed != section:
295+
return super().mouseReleaseEvent(event)
296+
if event.button() == Qt.LeftButton:
297+
opt = self.__viewItemOption(section)
298+
hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self)
299+
if hitrect.contains(pos):
300+
state = self.sectionCheckState(section)
301+
newstate = Qt.Checked if state == Qt.Unchecked else Qt.Unchecked
302+
model = self.model()
303+
model.setHeaderData(
304+
section, self.orientation(), newstate, Qt.CheckStateRole)
305+
return
306+
return super().mouseReleaseEvent(event)
307+
308+
def isSectionCheckable(self, index: int) -> bool:
309+
model = self.model()
310+
if model is None: # pragma: no cover
311+
return False
312+
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
313+
return checkstate is not None
314+
315+
def sectionCheckState(self, index: int) -> Qt.CheckState | None:
316+
model = self.model()
317+
if model is None: # pragma: no cover
318+
return None
319+
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
320+
if checkstate is None:
321+
return None
322+
try:
323+
return Qt.CheckState(checkstate)
324+
except TypeError: # pragma: no cover
325+
return None
326+
327+
def __viewItemOption(self, index: int) -> QStyleOptionViewItem:
328+
opt = QStyleOptionHeader()
329+
self.initStyleOption(opt)
330+
self.initStyleOptionForIndex(opt, index)
331+
pos = self.sectionViewportPosition(index)
332+
size = self.sectionSize(index)
333+
if self.orientation() == Qt.Horizontal:
334+
rect = QRect(pos, 0, size, self.height())
335+
else:
336+
rect = QRect(0, pos, self.width(), size)
337+
optindicator = QStyleOptionViewItem()
338+
optindicator.initFrom(self)
339+
optindicator.rect = rect
340+
optindicator.font = self.font()
341+
optindicator.fontMetrics = opt.fontMetrics
342+
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
343+
if not opt.icon.isNull():
344+
optindicator.icon = opt.icon
345+
optindicator.features |= QStyleOptionViewItem.HasDecoration
346+
return optindicator
347+
348+
def sectionSizeFromContents(self, logicalIndex: int) -> QSize:
349+
style = self.style()
350+
opt = QStyleOptionHeader()
351+
self.initStyleOption(opt)
352+
self.initStyleOptionForIndex(opt, logicalIndex)
353+
sh = style.sizeFromContents(QStyle.CT_HeaderSection, opt,
354+
QSize(), self)
355+
356+
optindicator = QStyleOptionViewItem()
357+
optindicator.initFrom(self)
358+
optindicator.font = self.font()
359+
optindicator.fontMetrics = opt.fontMetrics
360+
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
361+
optindicator.rect = opt.rect
362+
indicator_rect = style.subElementRect(
363+
QStyle.SE_ItemViewItemCheckIndicator, optindicator, self)
364+
return QSize(sh.width() + indicator_rect.width() + 4,
365+
max(sh.height(), indicator_rect.height()))

Orange/widgets/utils/tests/test_headerview.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
from Orange.widgets.tests.base import GuiTest
9-
from Orange.widgets.utils.headerview import HeaderView
9+
from Orange.widgets.utils.headerview import HeaderView, CheckableHeaderView
1010
from Orange.widgets.utils.textimport import StampIconEngine
1111

1212

@@ -103,3 +103,23 @@ def test_header_view_clickable(self):
103103
opt = QStyleOptionHeader()
104104
header.initStyleOptionForIndex(opt, 0)
105105
self.assertFalse(opt.state & QStyle.State_Sunken)
106+
107+
108+
class TestCheckableHeaderView(GuiTest):
109+
def test_view(self):
110+
model = QStandardItemModel()
111+
model.setColumnCount(1)
112+
model.setRowCount(3)
113+
view = CheckableHeaderView(Qt.Vertical)
114+
view.setModel(model)
115+
view.adjustSize()
116+
model.setHeaderData(0, Qt.Vertical, Qt.Checked, Qt.CheckStateRole)
117+
model.setHeaderData(1, Qt.Vertical, Qt.Unchecked, Qt.CheckStateRole)
118+
view.grab()
119+
style = view.style()
120+
opt = view._CheckableHeaderView__viewItemOption(0)
121+
hr = style.subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, view)
122+
QTest.mouseClick(view.viewport(), Qt.LeftButton, pos=hr.center())
123+
self.assertEqual(model.headerData(0, Qt.Vertical, Qt.CheckStateRole), Qt.Unchecked)
124+
QTest.mouseClick(view.viewport(), Qt.LeftButton, pos=hr.center())
125+
self.assertEqual(model.headerData(0, Qt.Vertical, Qt.CheckStateRole), Qt.Checked)

Orange/widgets/utils/textimport.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
)
5656

5757
from Orange.widgets.utils import encodings
58+
from Orange.widgets.utils.tableview import TableView
59+
from Orange.widgets.utils.headerview import CheckableHeaderView
5860
from Orange.widgets.utils.overlay import OverlayWidget
5961
from Orange.widgets.utils.combobox import TextEditCombo
6062

@@ -1339,13 +1341,16 @@ class RowSpec(enum.IntEnum):
13391341
Skipped = 2
13401342

13411343

1342-
class TablePreview(QTableView):
1344+
class TablePreview(TableView):
13431345
RowSpec = RowSpec
13441346
Header, Skipped = RowSpec
13451347

13461348
def __init__(self, *args, **kwargs):
13471349
super().__init__(*args, **kwargs)
13481350
self.setItemDelegate(PreviewItemDelegate(self))
1351+
header = CheckableHeaderView(Qt.Vertical)
1352+
header.setSectionsClickable(True)
1353+
self.setVerticalHeader(header)
13491354
self.horizontalHeader().viewport().installEventFilter(self)
13501355
self.verticalHeader().viewport().installEventFilter(self)
13511356
self.viewport().installEventFilter(self)
@@ -1396,9 +1401,8 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
13961401
bhv = QTableView.SelectColumns
13971402
elif obj is self.verticalHeader().viewport():
13981403
bhv = QTableView.SelectRows
1399-
elif event.button() in (Qt.LeftButton, Qt.RightButton) and \
1404+
if event.button() in (Qt.LeftButton, Qt.RightButton) and \
14001405
obj is self.viewport():
1401-
pass
14021406
bhv = QTableView.SelectColumns
14031407
if bhv != self.selectionBehavior():
14041408
self.clearSelection()
@@ -1681,6 +1685,9 @@ def headerData(self, section, orientation, role=Qt.DisplayRole):
16811685
"""Reimplemented."""
16821686
if role == Qt.DisplayRole:
16831687
return section + 1
1688+
elif role == Qt.CheckStateRole and orientation == Qt.Vertical:
1689+
state = self.__headerData[orientation][section].get(TablePreviewModel.RowStateRole)
1690+
return Qt.Unchecked if state == RowSpec.Skipped else Qt.Checked
16841691
else:
16851692
return self.__headerData[orientation][section].get(role)
16861693

@@ -1692,6 +1699,9 @@ def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
16921699
if value is None:
16931700
del self.__headerData[orientation][section][role]
16941701
else:
1702+
if role == Qt.CheckStateRole and orientation == Qt.Vertical:
1703+
role = TablePreviewModel.RowStateRole
1704+
value = RowSpec.Skipped if value == Qt.Unchecked else None
16951705
self.__headerData[orientation][section][role] = value
16961706
self.headerDataChanged.emit(orientation, section, section)
16971707
return True

0 commit comments

Comments
 (0)