wip
This commit is contained in:
@@ -1,37 +1,119 @@
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtCore import QModelIndex, QSize, Qt, Signal
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QKeySequence
|
||||
from qtpy.QtWidgets import QListWidget, QPushButton
|
||||
from qtpy.QtWidgets import QHeaderView, QPushButton, QStyle, QStyledItemDelegate, QTableView
|
||||
|
||||
_ROW_MARGIN = 2 # total vertical padding (top+bottom) added around the text block
|
||||
|
||||
|
||||
class MyListWidget(QListWidget):
|
||||
class _MultilineTextDelegate(QStyledItemDelegate):
|
||||
"""
|
||||
Paints and sizes a cell whose DisplayRole text may contain literal
|
||||
'\\n' line breaks (schedule item labels are built with explicit line
|
||||
breaks, e.g. the digital-twin config summary). The base
|
||||
QStyledItemDelegate only reliably shows a single line, so without this
|
||||
a multi-line label got clipped once rows stopped auto-sizing the way
|
||||
QListWidgetItem used to. `sizeHint()` reports the full multi-line
|
||||
height so the view's ResizeToContents row mode can actually fit it.
|
||||
|
||||
Selection is painted here with `option.palette.highlight()` - the
|
||||
same approach BecLogTableView's `_LogCellDelegate` uses - so the row
|
||||
gets the theme's native (blue) highlight color instead of whatever a
|
||||
stylesheet might otherwise hardcode.
|
||||
"""
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
painter.save()
|
||||
if option.state & QStyle.StateFlag.State_Selected:
|
||||
painter.fillRect(option.rect, option.palette.highlight())
|
||||
painter.setPen(option.palette.highlightedText().color())
|
||||
else:
|
||||
painter.setPen(option.palette.text().color())
|
||||
painter.setFont(option.font)
|
||||
|
||||
text = index.data(Qt.ItemDataRole.DisplayRole) or ""
|
||||
rect = option.rect.adjusted(4, _ROW_MARGIN // 2, -4, -(_ROW_MARGIN // 2))
|
||||
# painter.drawText(rect, int(Qt.TextFlag.TextWordWrap), text)
|
||||
painter.drawText(
|
||||
rect,
|
||||
int(
|
||||
Qt.TextFlag.TextWordWrap
|
||||
| Qt.AlignmentFlag.AlignLeft
|
||||
| Qt.AlignmentFlag.AlignVCenter
|
||||
),
|
||||
text,
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
text = index.data(Qt.ItemDataRole.DisplayRole) or ""
|
||||
lines = text.count("\n") + 1
|
||||
height = option.fontMetrics.lineSpacing() * lines + _ROW_MARGIN
|
||||
width = super().sizeHint(option, index).width()
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class MyTableView(QTableView):
|
||||
deletePressed = Signal()
|
||||
emptySpaceClicked = Signal()
|
||||
copyPressed = Signal()
|
||||
pastePressed = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
# Column layout is fixed: 0 = status icon, 1 = text (stretches), 2 = pause-on-failure icon.
|
||||
TEXT_COLUMN = 1
|
||||
|
||||
def __init__(self, parent=None, status_col_width: int = 60, pause_col_width: int = 130):
|
||||
super().__init__(parent)
|
||||
|
||||
self._status_col_width = status_col_width
|
||||
self._pause_col_width = pause_col_width
|
||||
|
||||
self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
self.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
||||
self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
self.setShowGrid(False)
|
||||
self.setWordWrap(True)
|
||||
|
||||
self.setItemDelegateForColumn(self.TEXT_COLUMN, _MultilineTextDelegate(self))
|
||||
|
||||
self.verticalHeader().hide()
|
||||
# ResizeToContents (not Fixed) so a row grows to fit however many
|
||||
# lines its text cell reports via the delegate's sizeHint above -
|
||||
# this is what lets multi-line item text show in full again.
|
||||
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
# No `:selected` rule here on purpose - leaving it out lets Qt fall
|
||||
# back to the palette highlight color (blue) for selected rows,
|
||||
# matching BecLogTableView instead of a hardcoded dark gray.
|
||||
self.setStyleSheet("""
|
||||
QListWidget::item {
|
||||
QTableView::item {
|
||||
border-bottom: 1px solid #444444;
|
||||
padding: 4px;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: #3a3a3a;
|
||||
border-bottom: 1px solid #444444;
|
||||
}
|
||||
""")
|
||||
""")
|
||||
|
||||
def configure_columns(self):
|
||||
header = self.horizontalHeader()
|
||||
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||
|
||||
header.resizeSection(0, self._status_col_width)
|
||||
header.resizeSection(2, self._pause_col_width)
|
||||
|
||||
header.setStretchLastSection(False)
|
||||
header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Delete and self.currentItem() is not None:
|
||||
if event.key() == Qt.Key_Delete and self.currentIndex().isValid():
|
||||
self.deletePressed.emit()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if event.matches(QKeySequence.StandardKey.Copy) and self.currentItem() is not None:
|
||||
if event.matches(QKeySequence.StandardKey.Copy) and self.currentIndex().isValid():
|
||||
self.copyPressed.emit()
|
||||
event.accept()
|
||||
return
|
||||
@@ -44,9 +126,12 @@ class MyListWidget(QListWidget):
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton and self.itemAt(event.position().toPoint()) is None:
|
||||
if (
|
||||
event.button() == Qt.LeftButton
|
||||
and not self.indexAt(event.position().toPoint()).isValid()
|
||||
):
|
||||
self.clearSelection()
|
||||
self.setCurrentRow(-1)
|
||||
self.setCurrentIndex(QModelIndex())
|
||||
self.emptySpaceClicked.emit()
|
||||
return
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ from bec_widgets.utils.error_popups import SafeSlot
|
||||
from pydantic import ValidationError
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Qt, QTimer, Signal
|
||||
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer, Signal
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidgetItem,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QVBoxLayout,
|
||||
@@ -58,16 +58,13 @@ from .notifications import (
|
||||
notify_item_finished,
|
||||
notify_schedule_state,
|
||||
)
|
||||
from .qt_widgets import MyButton, MyListWidget
|
||||
from .qt_widgets import MyButton, MyTableView
|
||||
from .schedule_item import Schedule, ScheduleItem
|
||||
from .schedule_logic import index_of, pick_next_runnable, protected_prefix_length
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
_ACTIVE_QUEUE_STATES = ("PENDING", "RUNNING")
|
||||
_ITEM_ID_ROLE = (
|
||||
Qt.ItemDataRole.UserRole + 1
|
||||
) # QListWidgetItem data role used to map a row back to an item_id
|
||||
|
||||
ICON_SIZE = 20
|
||||
_ICON_MAP = {
|
||||
@@ -79,6 +76,178 @@ _ICON_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def build_item_display_text(item: ScheduleItem, beamline: str | None) -> str:
|
||||
"""
|
||||
Build the human-readable label for a schedule item, pulled out of the
|
||||
old `_refresh_ui` QListWidgetItem-building loop so the table model's
|
||||
`data()` can call it too. Pure function of (item, beamline) - no Qt
|
||||
widgets, no locking - so it's safe to call from the model on every
|
||||
repaint.
|
||||
"""
|
||||
text = ""
|
||||
if item.form_state is not None:
|
||||
if item.command.startswith("scans.xas_simple_scan"):
|
||||
text = (
|
||||
f"XAS simple scan from {item.form_state['kwargs']['start']} eV to "
|
||||
+ f"{item.form_state['kwargs']['stop']} eV, {item.form_state['kwargs']['scan_time']}"
|
||||
+ f" s per spectrum, measure {item.form_state['kwargs']['scan_duration']}s"
|
||||
+ f"\n Comment: {item.form_state['kwargs']['metadata']['comment']}, "
|
||||
+ f"Sample Name: {item.form_state['kwargs']['metadata']['sample_name']}"
|
||||
)
|
||||
elif re.match(r"^dev\..*\.move", item.command):
|
||||
if item.form_state["relative"]:
|
||||
text = f"Move {item.form_state['device_name']} by {item.form_state['value']:+g}"
|
||||
else:
|
||||
text = f"Move {item.form_state['device_name']} to {item.form_state['value']}"
|
||||
elif item.command.startswith("dev.ic"):
|
||||
text = (
|
||||
f"Fill {item.form_state['chamber']} with {item.form_state['conc1']} % "
|
||||
+ f"{item.form_state['gas1']} and {item.form_state['conc2']} % "
|
||||
+ f"{item.form_state['gas2']} to {item.form_state['pressure']} bar"
|
||||
)
|
||||
elif item.command.startswith("dev.reffoilchanger"):
|
||||
text = f"Insert {item.form_state['foil']} reference foil"
|
||||
elif item.command.startswith("digital_twin.move_with_config({"):
|
||||
config = item.form_state["config"]
|
||||
if config["mo1_mode"] == "Monochromatic":
|
||||
text = f"Align Beamline for {config['energy']:.0f} eV"
|
||||
else:
|
||||
text = "Align Beamline for pinkbeam"
|
||||
text += (
|
||||
f"\n FE: {config['h_acc']*1e3:.3f} mrad x {config['v_acc']*1e3:.3f} mrad"
|
||||
+ f"\n CM: {config['cm_stripe']} at {config['cm_pitch']*1e3:.3f} mrad"
|
||||
)
|
||||
if config["mo1_mode"] == "Monochromatic":
|
||||
text += f"\n MO1: {config['mo1_xtal']}"
|
||||
text += f"\n FM: {config['fm_stripe']} at {config['fm_rotx']*1e3:.3f} mrad"
|
||||
if beamline == "x01da":
|
||||
text += f"\n ES1: {config['ot_es1_trz']:.0f} mm"
|
||||
text += f"\n SMPL_TRZ: {config['es1man_trz']:.3f} mm"
|
||||
if beamline == "x01da":
|
||||
text += f"\n SMPL-XRD: {config['smpl_to_xrd']:.3f} mm"
|
||||
elif item.command.startswith("auto_gain.start("):
|
||||
config = item.form_state
|
||||
text += f"Auto-configure gains for {item.form_state['element']}, {item.form_state['edge']}-edge"
|
||||
devices = ""
|
||||
for device in ["ic0", "ic1", "ic2", "pips"]:
|
||||
if config[device] is True:
|
||||
if len(devices) == 0:
|
||||
devices += f"\n for devices: {device}"
|
||||
else:
|
||||
devices += f", {device}"
|
||||
text += devices
|
||||
if text == "":
|
||||
text = f"{item.command}"
|
||||
if item.error:
|
||||
text += f" ({item.error.strip().splitlines()[-1]})"
|
||||
return text
|
||||
|
||||
|
||||
class ScheduleTableModel(QAbstractTableModel):
|
||||
"""
|
||||
Read-mostly table model over `Schedule.items`, rebuilt in one shot via
|
||||
`set_items()` whenever the Scheduler widget refreshes - the same
|
||||
"clear and repopulate" pattern the old QListWidget code used, just
|
||||
routed through Qt's model/view API instead of manual
|
||||
QListWidgetItem management. Mirrors the shape of `BecLogTableView`'s
|
||||
`BecLogsTableModel`: a flat Python list backing store, `data()`
|
||||
dispatching on `role`/`column`, icons returned as pixmaps via
|
||||
`DecorationRole`.
|
||||
"""
|
||||
|
||||
COL_STATUS = 0
|
||||
COL_TEXT = 1
|
||||
COL_PAUSE = 2
|
||||
_HEADERS = ("Status", "Item", "Pause on failure")
|
||||
|
||||
def __init__(self, parent=None, beamline: str | None = None):
|
||||
super().__init__(parent)
|
||||
self._items: list[ScheduleItem] = []
|
||||
self._beamline = beamline
|
||||
# Drives the "selected row -> white status icon" recolor; kept
|
||||
# separate from the view's own selection model so the icon color
|
||||
# survives a `set_items()` reset (which clears Qt's selection).
|
||||
self._selected_item_id: str | None = None
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 0 if parent.isValid() else len(self._items)
|
||||
|
||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 0 if parent.isValid() else len(self._HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
||||
return self._HEADERS[section]
|
||||
return None
|
||||
|
||||
def set_items(self, items: list[ScheduleItem]):
|
||||
self.beginResetModel()
|
||||
self._items = list(items)
|
||||
self.endResetModel()
|
||||
|
||||
def set_selected_item_id(self, item_id: str | None):
|
||||
"""
|
||||
Repaint just the status-icon column for the recolor - avoids a
|
||||
full model reset, which would otherwise drop the view's own
|
||||
selection/scroll state on every click.
|
||||
"""
|
||||
self._selected_item_id = item_id
|
||||
if self._items:
|
||||
top_left = self.index(0, self.COL_STATUS)
|
||||
bottom_right = self.index(len(self._items) - 1, self.COL_STATUS)
|
||||
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DecorationRole])
|
||||
|
||||
def item_id(self, row: int) -> str | None:
|
||||
if 0 <= row < len(self._items):
|
||||
return self._items[row].item_id
|
||||
return None
|
||||
|
||||
def row_of(self, item_id: str | None) -> int | None:
|
||||
if item_id is None:
|
||||
return None
|
||||
for row, item in enumerate(self._items):
|
||||
if item.item_id == item_id:
|
||||
return row
|
||||
return None
|
||||
|
||||
def item_at(self, row: int) -> ScheduleItem | None:
|
||||
return self._items[row] if 0 <= row < len(self._items) else None
|
||||
|
||||
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
item = self._items[index.row()]
|
||||
col = index.column()
|
||||
|
||||
if role == Qt.ItemDataRole.DecorationRole:
|
||||
if col == self.COL_STATUS:
|
||||
icon_name, color = _ICON_MAP[item.status]
|
||||
if item.item_id == self._selected_item_id:
|
||||
color = "#FFFFFF"
|
||||
return material_icon(
|
||||
icon_name, size=(ICON_SIZE, ICON_SIZE), color=color, convert_to_pixmap=True
|
||||
)
|
||||
if col == self.COL_PAUSE and item.pause_on_failure:
|
||||
return material_icon(
|
||||
"pause_circle",
|
||||
size=(ICON_SIZE, ICON_SIZE),
|
||||
color="#e6d922",
|
||||
convert_to_pixmap=True,
|
||||
)
|
||||
return None
|
||||
|
||||
if role == Qt.ItemDataRole.DisplayRole and col == self.COL_TEXT:
|
||||
return build_item_display_text(item, self._beamline)
|
||||
|
||||
if role == Qt.ItemDataRole.ToolTipRole:
|
||||
if col == self.COL_PAUSE:
|
||||
return "Pauses the schedule if this item fails" if item.pause_on_failure else None
|
||||
if item.error:
|
||||
return item.error
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ScheduleWidgetConfig(ConnectionConfig):
|
||||
"""
|
||||
Per-instance identity/settings for `ScheduleWidget`, following the same
|
||||
@@ -252,15 +421,18 @@ class Scheduler(BECWidget, QWidget):
|
||||
edit_row.addStretch()
|
||||
schedule_layout.addLayout(edit_row)
|
||||
|
||||
self.list_widget = MyListWidget()
|
||||
self.list_widget.currentRowChanged.connect(self._on_selection_changed)
|
||||
self.list_widget.deletePressed.connect(self._on_delete_clicked)
|
||||
self.list_widget.emptySpaceClicked.connect(self._on_empty_space_clicked)
|
||||
self.list_widget.itemDoubleClicked.connect(self._on_edit_clicked)
|
||||
self.list_widget.copyPressed.connect(self._on_copy_clicked)
|
||||
self.list_widget.pastePressed.connect(self._on_paste_clicked)
|
||||
self._table_model = ScheduleTableModel(parent=self, beamline=self.beamline)
|
||||
self.table_view = MyTableView()
|
||||
self.table_view.setModel(self._table_model)
|
||||
self.table_view.configure_columns()
|
||||
self.table_view.selectionModel().currentRowChanged.connect(self._on_selection_changed)
|
||||
self.table_view.deletePressed.connect(self._on_delete_clicked)
|
||||
self.table_view.emptySpaceClicked.connect(self._on_empty_space_clicked)
|
||||
self.table_view.doubleClicked.connect(self._on_edit_clicked)
|
||||
self.table_view.copyPressed.connect(self._on_copy_clicked)
|
||||
self.table_view.pastePressed.connect(self._on_paste_clicked)
|
||||
|
||||
schedule_layout.addWidget(self.list_widget)
|
||||
schedule_layout.addWidget(self.table_view)
|
||||
|
||||
schedule_layout.addWidget(QLabel("Notes"))
|
||||
self.notes_edit = QPlainTextEdit()
|
||||
@@ -472,78 +644,23 @@ class Scheduler(BECWidget, QWidget):
|
||||
|
||||
self.schedule_name_input.setText(schedule_name)
|
||||
|
||||
self.list_widget.blockSignals(True)
|
||||
self.list_widget.clear()
|
||||
selected_row = None
|
||||
for row, item in enumerate(items):
|
||||
text = ""
|
||||
if item.form_state is not None:
|
||||
if item.command.startswith("scans.xas_simple_scan"):
|
||||
text = (
|
||||
f"XAS simple scan from {item.form_state['kwargs']['start']} eV to "
|
||||
+ f"{item.form_state['kwargs']['stop']} eV, {item.form_state['kwargs']['scan_time']}"
|
||||
+ f" s per spectrum, measure {item.form_state['kwargs']['scan_duration']}s"
|
||||
+ f"\n Comment: {item.form_state['kwargs']['metadata']['comment']}, "
|
||||
+ f"Sample Name: {item.form_state['kwargs']['metadata']['sample_name']}"
|
||||
)
|
||||
elif re.match(r"^dev\..*\.move", item.command):
|
||||
if item.form_state["relative"]:
|
||||
text = f"Move {item.form_state['device_name']} by {item.form_state['value']:+g}"
|
||||
else:
|
||||
text = (
|
||||
f"Move {item.form_state['device_name']} to {item.form_state['value']}"
|
||||
)
|
||||
elif item.command.startswith("dev.ic"):
|
||||
text = (
|
||||
f"Fill {item.form_state['chamber']} with {item.form_state['conc1']} % "
|
||||
+ f"{item.form_state['gas1']} and {item.form_state['conc2']} % "
|
||||
+ f"{item.form_state['gas2']} to {item.form_state['pressure']} bar"
|
||||
)
|
||||
elif item.command.startswith("dev.reffoilchanger"):
|
||||
text = f"Insert {item.form_state['foil']} reference foil"
|
||||
elif item.command.startswith("digital_twin.move_with_config({"):
|
||||
config = item.form_state["config"]
|
||||
if config["mo1_mode"] == "Monochromatic":
|
||||
text = f"Align Beamline for {config['energy']:.0f} eV"
|
||||
else:
|
||||
text = "Align Beamline for pinkbeam"
|
||||
text += (
|
||||
f"\n FE: {config['h_acc']*1e3:.3f} mrad x {config['v_acc']*1e3:.3f} mrad"
|
||||
+ f"\n CM: {config['cm_stripe']} at {config['cm_pitch']*1e3:.3f} mrad"
|
||||
)
|
||||
if config["mo1_mode"] == "Monochromatic":
|
||||
text += f"\n MO1: {config['mo1_xtal']}"
|
||||
text += f"\n FM: {config['fm_stripe']} at {config['fm_rotx']*1e3:.3f} mrad"
|
||||
if self.beamline == "x01da":
|
||||
text += f"\n ES1: {config['ot_es1_trz']:.0f} mm"
|
||||
text += f"\n SMPL_TRZ: {config['es1man_trz']:.3f} mm"
|
||||
if self.beamline == "x01da":
|
||||
text += f"\n SMPL-XRD: {config['smpl_to_xrd']:.3f} mm"
|
||||
elif item.command.startswith("auto_gain.start("):
|
||||
config = item.form_state
|
||||
text += f"Auto-configure gains for {item.form_state['element']}, {item.form_state['edge']}-edge"
|
||||
devices = ""
|
||||
for device in ["ic0", "ic1", "ic2", "pips"]:
|
||||
if config[device] is True:
|
||||
if len(devices) == 0:
|
||||
devices += f"\n for devices: {device}"
|
||||
else:
|
||||
devices += f", {device}"
|
||||
text += devices
|
||||
if text == "":
|
||||
text = f"{item.command}"
|
||||
if item.error:
|
||||
text += f" ({item.error.strip().splitlines()[-1]})"
|
||||
list_item = QListWidgetItem(text, self.list_widget)
|
||||
list_item.setData(_ITEM_ID_ROLE, item.item_id)
|
||||
if item.item_id == self._selected_item_id:
|
||||
selected_row = row
|
||||
self._update_icons()
|
||||
if selected_row is not None:
|
||||
self.list_widget.setCurrentRow(selected_row)
|
||||
selection_model = self.table_view.selectionModel()
|
||||
selection_model.blockSignals(True)
|
||||
|
||||
# Repopulate the model (full reset, same "clear and rebuild" idea
|
||||
# the old QListWidget code used); the selected item's status-icon
|
||||
# color is baked in via `_selected_item_id` so the highlighted row
|
||||
# keeps its white icon straight after the reset repaints it.
|
||||
self._table_model._selected_item_id = self._selected_item_id
|
||||
self._table_model.set_items(items)
|
||||
|
||||
row = self._table_model.row_of(self._selected_item_id)
|
||||
if row is not None:
|
||||
self.table_view.selectRow(row)
|
||||
else:
|
||||
self._selected_item_id = None
|
||||
self.list_widget.blockSignals(False)
|
||||
|
||||
selection_model.blockSignals(False)
|
||||
|
||||
# Don't stomp on notes the operator is actively typing (e.g. a
|
||||
# remote update, or our own debounce timer firing right after
|
||||
@@ -554,6 +671,8 @@ class Scheduler(BECWidget, QWidget):
|
||||
self.notes_edit.setPlainText(notes)
|
||||
self.notes_edit.blockSignals(False)
|
||||
|
||||
self._update_buttons()
|
||||
|
||||
def _update_guard_label(self, *_):
|
||||
if self._guard.enabled:
|
||||
state = "OK" if self._guard.is_clear() else "PAUSED - waiting to resume"
|
||||
@@ -600,18 +719,6 @@ class Scheduler(BECWidget, QWidget):
|
||||
self.copy_btn.setEnabled(selected_item is not None)
|
||||
self.paste_btn.setEnabled(self._clipboard_item is not None)
|
||||
|
||||
def _update_icons(self):
|
||||
items = list(self.schedule.items)
|
||||
for row, item in enumerate(items):
|
||||
icon_name, color = _ICON_MAP[item.status]
|
||||
if self._selected_item_id == item.item_id:
|
||||
color = "#FFFFFF"
|
||||
icon = material_icon(
|
||||
icon_name, size=(ICON_SIZE, ICON_SIZE), color=color, convert_to_pixmap=True
|
||||
)
|
||||
list_item = self.list_widget.item(row)
|
||||
list_item.setIcon(icon)
|
||||
|
||||
@SafeSlot()
|
||||
def _on_schedule_name_change(self):
|
||||
name = self.schedule_name_input.text()
|
||||
@@ -619,16 +726,16 @@ class Scheduler(BECWidget, QWidget):
|
||||
self.schedule.schedule_name = name
|
||||
self._persist_locked()
|
||||
|
||||
@SafeSlot()
|
||||
def _on_selection_changed(self, row: int):
|
||||
item = self.list_widget.item(row) if row >= 0 else None
|
||||
self._selected_item_id = item.data(_ITEM_ID_ROLE) if item is not None else None
|
||||
self._update_icons()
|
||||
@SafeSlot(QModelIndex, QModelIndex)
|
||||
def _on_selection_changed(self, current: QModelIndex, previous: QModelIndex):
|
||||
item_id = self._table_model.item_id(current.row()) if current.isValid() else None
|
||||
self._selected_item_id = item_id
|
||||
self._table_model.set_selected_item_id(item_id)
|
||||
self._update_buttons()
|
||||
|
||||
@SafeSlot()
|
||||
def _on_empty_space_clicked(self):
|
||||
self.list_widget.setCurrentRow(-1)
|
||||
self.table_view.setCurrentIndex(QModelIndex())
|
||||
self._refresh_ui()
|
||||
|
||||
# ---- notes ---- #
|
||||
@@ -857,6 +964,7 @@ class Scheduler(BECWidget, QWidget):
|
||||
self._update_buttons()
|
||||
|
||||
@SafeSlot()
|
||||
@SafeSlot(QModelIndex)
|
||||
def _on_edit_clicked(self, *_):
|
||||
if self._selected_item_id is None:
|
||||
return
|
||||
@@ -904,14 +1012,19 @@ class Scheduler(BECWidget, QWidget):
|
||||
def _on_delete_clicked(self):
|
||||
if self._selected_item_id is None:
|
||||
return
|
||||
row = self.list_widget.currentRow()
|
||||
row = self.table_view.currentIndex().row()
|
||||
try:
|
||||
self.delete_item(self._selected_item_id)
|
||||
except RuntimeError as exc:
|
||||
QMessageBox.warning(self, "Cannot delete item", str(exc))
|
||||
return
|
||||
self._selected_item_id = None
|
||||
self._on_selection_changed(max(0, row - 1))
|
||||
# delete_item()'s schedule_changed emit already ran _refresh_ui()
|
||||
# with an empty selection; now re-select the row that took the
|
||||
# deleted item's place (or the new last row, if it was last).
|
||||
new_row = max(0, row - 1)
|
||||
if 0 <= new_row < self._table_model.rowCount():
|
||||
self.table_view.selectRow(new_row)
|
||||
self._refresh_ui()
|
||||
|
||||
@SafeSlot()
|
||||
|
||||
Reference in New Issue
Block a user