97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
"""InstrumentDelegate editing UX: styled editor, validation, Tab-move, hover chip."""
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import QLineEdit, QStyle, QTableWidget, QTableWidgetItem
|
|
|
|
from cim_suite.core.ui.kit import CellKind, InstrumentDelegate
|
|
from cim_suite.core.ui.theme import current
|
|
|
|
|
|
def _table(qtbot, rows, editable):
|
|
"""editable: set of (row, col) that keep Qt.ItemIsEditable."""
|
|
table = QTableWidget(len(rows), len(rows[0]))
|
|
qtbot.addWidget(table)
|
|
for r, row in enumerate(rows):
|
|
for c, text in enumerate(row):
|
|
item = QTableWidgetItem(text)
|
|
if (r, c) not in editable:
|
|
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
table.setItem(r, c, item)
|
|
delegate = InstrumentDelegate(table)
|
|
table.setItemDelegate(delegate)
|
|
table.show()
|
|
return table, delegate
|
|
|
|
|
|
def _editor(table):
|
|
editor = table.findChild(QLineEdit, "CellEditor")
|
|
assert editor is not None
|
|
return editor
|
|
|
|
|
|
def test_editor_is_the_styled_cell_editor(qtbot):
|
|
table, _ = _table(qtbot, [["a"]], {(0, 0)})
|
|
table.editItem(table.item(0, 0))
|
|
editor = _editor(table)
|
|
assert editor.objectName() == "CellEditor"
|
|
|
|
|
|
def test_invalid_input_blocks_enter_until_fixed(qtbot):
|
|
table, delegate = _table(qtbot, [["5"]], {(0, 0)})
|
|
delegate.set_column_validator(
|
|
0, lambda text: None if text.strip().isdigit() else "whole numbers only"
|
|
)
|
|
table.editItem(table.item(0, 0))
|
|
editor = _editor(table)
|
|
editor.setText("abc")
|
|
qtbot.keyClick(editor, Qt.Key.Key_Return)
|
|
assert editor.property("state") == "invalid"
|
|
assert editor.toolTip() == "whole numbers only"
|
|
assert table.item(0, 0).text() == "5" # commit was blocked
|
|
editor.setText("12") # textChanged clears the invalid state
|
|
assert editor.property("state") == "valid"
|
|
qtbot.keyClick(editor, Qt.Key.Key_Return)
|
|
assert table.item(0, 0).text() == "12"
|
|
|
|
|
|
def test_tab_saves_and_moves_to_next_editable_cell(qtbot):
|
|
# (0,0) and (1,0) editable; (0,1) and (1,1) read-only — Tab must skip them.
|
|
table, _ = _table(qtbot, [["a", "ro"], ["b", "ro"]], {(0, 0), (1, 0)})
|
|
table.editItem(table.item(0, 0))
|
|
editor = _editor(table)
|
|
editor.setText("edited")
|
|
qtbot.keyClick(editor, Qt.Key.Key_Tab)
|
|
assert table.item(0, 0).text() == "edited" # Tab saved
|
|
assert (table.currentRow(), table.currentColumn()) == (1, 0) # skipped the RO cell
|
|
assert table.state() == table.State.EditingState # and opened the editor
|
|
|
|
|
|
def test_shift_tab_moves_backwards(qtbot):
|
|
table, _ = _table(qtbot, [["a", "ro"], ["b", "ro"]], {(0, 0), (1, 0)})
|
|
table.editItem(table.item(1, 0))
|
|
editor = _editor(table)
|
|
qtbot.keyClick(editor, Qt.Key.Key_Backtab)
|
|
assert (table.currentRow(), table.currentColumn()) == (0, 0)
|
|
|
|
|
|
def test_escape_cancels(qtbot):
|
|
table, _ = _table(qtbot, [["keep"]], {(0, 0)})
|
|
table.editItem(table.item(0, 0))
|
|
editor = _editor(table)
|
|
editor.setText("discard me")
|
|
qtbot.keyClick(editor, Qt.Key.Key_Escape)
|
|
assert table.item(0, 0).text() == "keep"
|
|
|
|
|
|
def test_hover_chip_only_on_editable_cells(qtbot, render_cell, near):
|
|
table, delegate = _table(qtbot, [["v", "ro"]], {(0, 0)})
|
|
delegate.set_column_kind(0, CellKind.NUMERIC)
|
|
delegate.set_column_kind(1, CellKind.NUMERIC)
|
|
hover = QStyle.StateFlag.State_MouseOver | QStyle.StateFlag.State_Enabled
|
|
editable_img = render_cell(delegate, table.model().index(0, 0), state=hover)
|
|
readonly_img = render_cell(delegate, table.model().index(0, 1), state=hover)
|
|
# §5.6: the chip appears on the editable cell; its absence IS the RO affordance.
|
|
assert near(editable_img.pixelColor(60, 6), current().accent_soft, tol=12)
|
|
assert near(readonly_img.pixelColor(60, 6), current().surface, tol=8)
|
|
assert editable_img.pixelColor(60, 6) != readonly_img.pixelColor(60, 6)
|