Skip to content

Commit

Permalink
Merge pull request #6691 from ales-erjavec/owcsvimport-skip-multirow
Browse files Browse the repository at this point in the history
[ENH] CSV Import: Skip multi rows edit
  • Loading branch information
VesnaT committed Jan 5, 2024
2 parents f1dd3fe + 1e768d2 commit 7233533
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 70 deletions.
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__)

0 comments on commit 7233533

Please sign in to comment.