Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 141 additions & 2 deletions Orange/widgets/utils/headerview.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from AnyQt.QtCore import Qt, QRect
from __future__ import annotations

from AnyQt.QtCore import Qt, QRect, QSize
from AnyQt.QtGui import QBrush, QIcon, QCursor, QPalette, QPainter, QMouseEvent
from AnyQt.QtWidgets import (
QHeaderView, QStyleOptionHeader, QStyle, QApplication
QHeaderView, QStyleOptionHeader, QStyle, QApplication, QStyleOptionViewItem
)


Expand Down Expand Up @@ -225,3 +227,140 @@ def paintSection(self, painter, rect, logicalIndex):
self.style().drawControl(QStyle.CE_Header, opt, painter, self)

painter.setBrushOrigin(oldBO)


class CheckableHeaderView(HeaderView):
"""
A HeaderView with checkable header items.

The header is checkable if the model defines a `Qt.CheckStateRole` value.
"""
__sectionPressed: int = -1

def paintSection(
self, painter: QPainter, rect: QRect, logicalIndex: int
) -> None:
opt = QStyleOptionHeader()
self.initStyleOption(opt)
self.initStyleOptionForIndex(opt, logicalIndex)
model = self.model()
if model is None:
return # pragma: no cover
opt.rect = rect
checkstate = self.sectionCheckState(logicalIndex)
ischeckable = checkstate is not None
style = self.style()
# draw background
style.drawControl(QStyle.CE_HeaderSection, opt, painter, self)
text_rect = QRect(rect)
optindicator = QStyleOptionViewItem()
optindicator.initFrom(self)
optindicator.font = self.font()
optindicator.fontMetrics = opt.fontMetrics
optindicator.features = QStyleOptionViewItem.HasCheckIndicator | QStyleOptionViewItem.HasDisplay
optindicator.rect = opt.rect
indicator_rect = style.subElementRect(
QStyle.SE_ItemViewItemCheckIndicator, optindicator, self)
text_rect.setLeft(indicator_rect.right() + 4)
if ischeckable:
optindicator.checkState = checkstate
optindicator.state |= QStyle.State_On if checkstate == Qt.Checked else QStyle.State_Off
optindicator.rect = indicator_rect
style.drawPrimitive(QStyle.PE_IndicatorItemViewItemCheck, optindicator,
painter, self)
opt.rect = text_rect
# draw section label
style.drawControl(QStyle.CE_HeaderLabel, opt, painter, self)

def mousePressEvent(self, event: QMouseEvent) -> None:
pos = event.pos()
section = self.logicalIndexAt(pos)
if section == -1 or not self.isSectionCheckable(section):
super().mousePressEvent(event)
return
if event.button() == Qt.LeftButton:
opt = self.__viewItemOption(section)
hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self)
if hitrect.contains(pos):
self.__sectionPressed = section
event.accept()
return
super().mousePressEvent(event)

def mouseReleaseEvent(self, event: QMouseEvent) -> None:
pos = event.pos()
section = self.logicalIndexAt(pos)
if section == -1 or not self.isSectionCheckable(section) \
or self.__sectionPressed != section:
super().mouseReleaseEvent(event)
return
if event.button() == Qt.LeftButton:
opt = self.__viewItemOption(section)
hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self)
if hitrect.contains(pos):
state = self.sectionCheckState(section)
newstate = Qt.Checked if state == Qt.Unchecked else Qt.Unchecked
model = self.model()
model.setHeaderData(
section, self.orientation(), newstate, Qt.CheckStateRole)
return
super().mouseReleaseEvent(event)

def isSectionCheckable(self, index: int) -> bool:
model = self.model()
if model is None: # pragma: no cover
return False
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
return checkstate is not None

def sectionCheckState(self, index: int) -> Qt.CheckState | None:
model = self.model()
if model is None: # pragma: no cover
return None
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
if checkstate is None:
return None
try:
return Qt.CheckState(checkstate)
except TypeError: # pragma: no cover
return None

def __viewItemOption(self, index: int) -> QStyleOptionViewItem:
opt = QStyleOptionHeader()
self.initStyleOption(opt)
self.initStyleOptionForIndex(opt, index)
pos = self.sectionViewportPosition(index)
size = self.sectionSize(index)
if self.orientation() == Qt.Horizontal:
rect = QRect(pos, 0, size, self.height())
else:
rect = QRect(0, pos, self.width(), size)
optindicator = QStyleOptionViewItem()
optindicator.initFrom(self)
optindicator.rect = rect
optindicator.font = self.font()
optindicator.fontMetrics = opt.fontMetrics
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
if not opt.icon.isNull():
optindicator.icon = opt.icon
optindicator.features |= QStyleOptionViewItem.HasDecoration
return optindicator

def sectionSizeFromContents(self, logicalIndex: int) -> QSize:
style = self.style()
opt = QStyleOptionHeader()
self.initStyleOption(opt)
self.initStyleOptionForIndex(opt, logicalIndex)
sh = style.sizeFromContents(QStyle.CT_HeaderSection, opt,
QSize(), self)

optindicator = QStyleOptionViewItem()
optindicator.initFrom(self)
optindicator.font = self.font()
optindicator.fontMetrics = opt.fontMetrics
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
optindicator.rect = opt.rect
indicator_rect = style.subElementRect(
QStyle.SE_ItemViewItemCheckIndicator, optindicator, self)
return QSize(sh.width() + indicator_rect.width() + 4,
max(sh.height(), indicator_rect.height()))
22 changes: 21 additions & 1 deletion Orange/widgets/utils/tests/test_headerview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


from Orange.widgets.tests.base import GuiTest
from Orange.widgets.utils.headerview import HeaderView
from Orange.widgets.utils.headerview import HeaderView, CheckableHeaderView
from Orange.widgets.utils.textimport import StampIconEngine


Expand Down Expand Up @@ -103,3 +103,23 @@ def test_header_view_clickable(self):
opt = QStyleOptionHeader()
header.initStyleOptionForIndex(opt, 0)
self.assertFalse(opt.state & QStyle.State_Sunken)


class TestCheckableHeaderView(GuiTest):
def test_view(self):
model = QStandardItemModel()
model.setColumnCount(1)
model.setRowCount(3)
view = CheckableHeaderView(Qt.Vertical)
view.setModel(model)
view.adjustSize()
model.setHeaderData(0, Qt.Vertical, Qt.Checked, Qt.CheckStateRole)
model.setHeaderData(1, Qt.Vertical, Qt.Unchecked, Qt.CheckStateRole)
view.grab()
style = view.style()
opt = view._CheckableHeaderView__viewItemOption(0)
hr = style.subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, view)
QTest.mouseClick(view.viewport(), Qt.LeftButton, pos=hr.center())
self.assertEqual(model.headerData(0, Qt.Vertical, Qt.CheckStateRole), Qt.Unchecked)
QTest.mouseClick(view.viewport(), Qt.LeftButton, pos=hr.center())
self.assertEqual(model.headerData(0, Qt.Vertical, Qt.CheckStateRole), Qt.Checked)
23 changes: 21 additions & 2 deletions Orange/widgets/utils/tests/test_textimport.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import unittest
import csv
import io
from AnyQt.QtCore import Qt

from AnyQt.QtWidgets import QComboBox, QWidget
from AnyQt.QtTest import QSignalSpy
from AnyQt.QtTest import QSignalSpy, QTest

from Orange.widgets.utils import textimport
from Orange.widgets.tests.base import GuiTest
from Orange.widgets.utils.textimport import TablePreview, TablePreviewModel

ColumnTypes = textimport.ColumnType

Expand All @@ -19,7 +21,7 @@
DATA5 = b'a\tb\n' * 1000


class WidgetsTests(GuiTest):
class OptionsWidgetTests(GuiTest):
def test_options_widget(self):
w = textimport.CSVOptionsWidget()
schanged = QSignalSpy(w.optionsChanged)
Expand Down Expand Up @@ -52,6 +54,8 @@ def test_options_widget(self):
self.assertEqual(d.delimiter, d1.delimiter)
self.assertEqual(d.quotechar, d1.quotechar)


class ImportWidgetTest(GuiTest):
def test_import_widget(self):
w = textimport.CSVImportWidget()
w.setDialect(csv.excel())
Expand Down Expand Up @@ -101,6 +105,21 @@ def test_import_widget(self):
self.assertGreater(model.rowCount(), rows)
self.assertEqual(len(spy), 1)

def test_preview_view(self):
w = TablePreview()
model = TablePreviewModel()
model.setPreviewStream(csv.reader(io.StringIO(DATA4.decode('utf-8'))))
w.setModel(model)
QTest.mouseClick(w.verticalHeader().viewport(), Qt.LeftButton)
self.assertEqual(w.selectionBehavior(), TablePreview.SelectRows)
QTest.mouseClick(w.horizontalHeader().viewport(), Qt.LeftButton)
self.assertEqual(w.selectionBehavior(), TablePreview.SelectColumns)

QTest.mouseClick(w.verticalHeader().viewport(), Qt.LeftButton)
self.assertEqual(w.selectionBehavior(), TablePreview.SelectRows)
QTest.mouseClick(w.viewport(), Qt.LeftButton)
self.assertEqual(w.selectionBehavior(), TablePreview.SelectColumns)


if __name__ == "__main__":
unittest.main(__name__)
Loading