Skip to content

Commit 784ecb1

Browse files
VesnaTjanezd
authored andcommitted
Merge pull request biolab#6691 from ales-erjavec/owcsvimport-skip-multirow
[ENH] CSV Import: Skip multi rows edit
2 parents f1dd3fe + 1e768d2 commit 784ecb1

File tree

6 files changed

+294
-71
lines changed

6 files changed

+294
-71
lines changed

Orange/widgets/data/owcorrelations.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,14 @@ def row_for_state(self, score, state):
156156
for attr in attrs:
157157
item = QStandardItem(attr.name)
158158
item.setData(attrs, self._AttrRole)
159-
item.setData(Qt.AlignLeft + Qt.AlignCenter, Qt.TextAlignmentRole)
159+
item.setData((Qt.AlignRight if not attr_items else Qt.AlignLeft)
160+
+ Qt.AlignVCenter, Qt.TextAlignmentRole)
160161
item.setToolTip(attr.name)
161162
attr_items.append(item)
163+
if len(attr_items) == 1:
164+
colon = QStandardItem(":")
165+
colon.setData(Qt.AlignCenter, Qt.TextAlignmentRole)
166+
attr_items.append(colon)
162167
correlation_item = QStandardItem("{:+.3f}".format(score[1]))
163168
correlation_item.setData(score[2], self.PValRole)
164169
correlation_item.setData(attrs, self._AttrRole)
@@ -195,6 +200,7 @@ def stopped(self):
195200
self.threadStopped.emit()
196201
header = self.rank_table.horizontalHeader()
197202
header.setSectionResizeMode(1, QHeaderView.Stretch)
203+
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
198204

199205
def start(self, task, *args, **kwargs):
200206
self._set_empty_status()

Orange/widgets/utils/headerview.py

Lines changed: 141 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,140 @@ 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+
super().mousePressEvent(event)
280+
return
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+
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+
super().mouseReleaseEvent(event)
296+
return
297+
if event.button() == Qt.LeftButton:
298+
opt = self.__viewItemOption(section)
299+
hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self)
300+
if hitrect.contains(pos):
301+
state = self.sectionCheckState(section)
302+
newstate = Qt.Checked if state == Qt.Unchecked else Qt.Unchecked
303+
model = self.model()
304+
model.setHeaderData(
305+
section, self.orientation(), newstate, Qt.CheckStateRole)
306+
return
307+
super().mouseReleaseEvent(event)
308+
309+
def isSectionCheckable(self, index: int) -> bool:
310+
model = self.model()
311+
if model is None: # pragma: no cover
312+
return False
313+
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
314+
return checkstate is not None
315+
316+
def sectionCheckState(self, index: int) -> Qt.CheckState | None:
317+
model = self.model()
318+
if model is None: # pragma: no cover
319+
return None
320+
checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole)
321+
if checkstate is None:
322+
return None
323+
try:
324+
return Qt.CheckState(checkstate)
325+
except TypeError: # pragma: no cover
326+
return None
327+
328+
def __viewItemOption(self, index: int) -> QStyleOptionViewItem:
329+
opt = QStyleOptionHeader()
330+
self.initStyleOption(opt)
331+
self.initStyleOptionForIndex(opt, index)
332+
pos = self.sectionViewportPosition(index)
333+
size = self.sectionSize(index)
334+
if self.orientation() == Qt.Horizontal:
335+
rect = QRect(pos, 0, size, self.height())
336+
else:
337+
rect = QRect(0, pos, self.width(), size)
338+
optindicator = QStyleOptionViewItem()
339+
optindicator.initFrom(self)
340+
optindicator.rect = rect
341+
optindicator.font = self.font()
342+
optindicator.fontMetrics = opt.fontMetrics
343+
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
344+
if not opt.icon.isNull():
345+
optindicator.icon = opt.icon
346+
optindicator.features |= QStyleOptionViewItem.HasDecoration
347+
return optindicator
348+
349+
def sectionSizeFromContents(self, logicalIndex: int) -> QSize:
350+
style = self.style()
351+
opt = QStyleOptionHeader()
352+
self.initStyleOption(opt)
353+
self.initStyleOptionForIndex(opt, logicalIndex)
354+
sh = style.sizeFromContents(QStyle.CT_HeaderSection, opt,
355+
QSize(), self)
356+
357+
optindicator = QStyleOptionViewItem()
358+
optindicator.initFrom(self)
359+
optindicator.font = self.font()
360+
optindicator.fontMetrics = opt.fontMetrics
361+
optindicator.features = QStyleOptionViewItem.HasCheckIndicator
362+
optindicator.rect = opt.rect
363+
indicator_rect = style.subElementRect(
364+
QStyle.SE_ItemViewItemCheckIndicator, optindicator, self)
365+
return QSize(sh.width() + indicator_rect.width() + 4,
366+
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/tests/test_textimport.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import unittest
22
import csv
33
import io
4+
from AnyQt.QtCore import Qt
45

56
from AnyQt.QtWidgets import QComboBox, QWidget
6-
from AnyQt.QtTest import QSignalSpy
7+
from AnyQt.QtTest import QSignalSpy, QTest
78

89
from Orange.widgets.utils import textimport
910
from Orange.widgets.tests.base import GuiTest
11+
from Orange.widgets.utils.textimport import TablePreview, TablePreviewModel
1012

1113
ColumnTypes = textimport.ColumnType
1214

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

2123

22-
class WidgetsTests(GuiTest):
24+
class OptionsWidgetTests(GuiTest):
2325
def test_options_widget(self):
2426
w = textimport.CSVOptionsWidget()
2527
schanged = QSignalSpy(w.optionsChanged)
@@ -52,6 +54,8 @@ def test_options_widget(self):
5254
self.assertEqual(d.delimiter, d1.delimiter)
5355
self.assertEqual(d.quotechar, d1.quotechar)
5456

57+
58+
class ImportWidgetTest(GuiTest):
5559
def test_import_widget(self):
5660
w = textimport.CSVImportWidget()
5761
w.setDialect(csv.excel())
@@ -101,6 +105,21 @@ def test_import_widget(self):
101105
self.assertGreater(model.rowCount(), rows)
102106
self.assertEqual(len(spy), 1)
103107

108+
def test_preview_view(self):
109+
w = TablePreview()
110+
model = TablePreviewModel()
111+
model.setPreviewStream(csv.reader(io.StringIO(DATA4.decode('utf-8'))))
112+
w.setModel(model)
113+
QTest.mouseClick(w.verticalHeader().viewport(), Qt.LeftButton)
114+
self.assertEqual(w.selectionBehavior(), TablePreview.SelectRows)
115+
QTest.mouseClick(w.horizontalHeader().viewport(), Qt.LeftButton)
116+
self.assertEqual(w.selectionBehavior(), TablePreview.SelectColumns)
117+
118+
QTest.mouseClick(w.verticalHeader().viewport(), Qt.LeftButton)
119+
self.assertEqual(w.selectionBehavior(), TablePreview.SelectRows)
120+
QTest.mouseClick(w.viewport(), Qt.LeftButton)
121+
self.assertEqual(w.selectionBehavior(), TablePreview.SelectColumns)
122+
104123

105124
if __name__ == "__main__":
106125
unittest.main(__name__)

0 commit comments

Comments
 (0)