Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] CSV Import: Skip multi rows edit #6691

Merged
merged 5 commits into from
Jan 5, 2024
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 @@
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)

Check warning on line 288 in Orange/widgets/utils/headerview.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/utils/headerview.py#L288

Added line #L288 was not covered by tests

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)

Check warning on line 307 in Orange/widgets/utils/headerview.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/utils/headerview.py#L307

Added line #L307 was not covered by tests

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())

Check warning on line 335 in Orange/widgets/utils/headerview.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/utils/headerview.py#L335

Added line #L335 was not covered by tests
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

Check warning on line 346 in Orange/widgets/utils/headerview.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/utils/headerview.py#L345-L346

Added lines #L345 - L346 were not covered by tests
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
Loading