diff --git a/bec_widgets/widgets/utility/logpanel/logpanel.py b/bec_widgets/widgets/utility/logpanel/logpanel.py index dcd93f56..da876c67 100644 --- a/bec_widgets/widgets/utility/logpanel/logpanel.py +++ b/bec_widgets/widgets/utility/logpanel/logpanel.py @@ -43,7 +43,7 @@ from qtpy.QtWidgets import ( QDateTimeEdit, QDialog, QDialogButtonBox, - QFormLayout, + QGridLayout, QHBoxLayout, QHeaderView, QLabel, @@ -982,6 +982,8 @@ class LogPanelToolbar(QWidget): self._known_services: set[str] = set() # None means "all services" - the include-list has not been narrowed self._checked_services: set[str] | None = None + self._active_start: QDateTime | None = None + self._active_end: QDateTime | None = None self._layout = QHBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) @@ -1166,57 +1168,77 @@ class LogPanelToolbar(QWidget): def _apply_time_preset(self, label: str, seconds: int | None): if seconds is None: - self.timestamp_update.emit(TimestampUpdate(value=None, update_type="start")) - self.timestamp_update.emit(TimestampUpdate(value=None, update_type="end")) - self.timerange_button.setText("All time") + self._set_time_range(None, None) return - start = QDateTime.currentDateTime().addSecs(-seconds) - self.timestamp_update.emit(TimestampUpdate(value=start, update_type="start")) - self.timestamp_update.emit(TimestampUpdate(value=None, update_type="end")) - self.timerange_button.setText(f">= {start.toString('HH:mm:ss')}") + self._set_time_range(QDateTime.currentDateTime().addSecs(-seconds), None) - @SafeSlot() - def _open_custom_range_dialog(self): - """One dialog with both bounds - replaces the former nested calendar dialogs.""" + def _set_time_range(self, start: QDateTime | None, end: QDateTime | None): + """Apply both bounds, remember them for the custom dialog, and label the button.""" + self._active_start = start + self._active_end = end + self.timestamp_update.emit(TimestampUpdate(value=start, update_type="start")) + self.timestamp_update.emit(TimestampUpdate(value=end, update_type="end")) + if start and end: + text = f"{start.toString('HH:mm')} - {end.toString('HH:mm')}" + elif start: + text = f">= {start.toString('HH:mm:ss')}" + elif end: + text = f"<= {end.toString('HH:mm:ss')}" + else: + text = "All time" + self.timerange_button.setText(text) + + def _build_custom_range_dialog( + self, + ) -> tuple[QDialog, dict[str, tuple[QCheckBox, QDateTimeEdit]]]: + """Build the custom range dialog, prefilled with the currently active bounds.""" dialog = QDialog(self) dialog.setWindowTitle("Time range") - form = QFormLayout(dialog) + layout = QGridLayout(dialog) + layout.setContentsMargins(16, 16, 16, 12) + layout.setHorizontalSpacing(12) + layout.setVerticalSpacing(10) bounds: dict[str, tuple[QCheckBox, QDateTimeEdit]] = {} - for bound, default in [ - ("start", QDateTime.currentDateTime().addSecs(-3600)), - ("end", QDateTime.currentDateTime()), - ]: - enable = QCheckBox("From" if bound == "start" else "Until", dialog) - edit = QDateTimeEdit(default, dialog) + rows = [ + ("start", "From", self._active_start, QDateTime.currentDateTime().addSecs(-3600)), + ("end", "Until", self._active_end, QDateTime.currentDateTime()), + ] + for row, (bound, label, active, default) in enumerate(rows): + enable = QCheckBox(label, dialog) + edit = QDateTimeEdit(active or default, dialog) edit.setCalendarPopup(True) edit.setDisplayFormat("yyyy-MM-dd HH:mm:ss") - edit.setEnabled(False) + edit.setMinimumWidth(220) + edit.setEnabled(active is not None) + enable.setChecked(active is not None) enable.toggled.connect(edit.setEnabled) - form.addRow(enable, edit) + layout.addWidget(enable, row, 0) + layout.addWidget(edit, row, 1) bounds[bound] = (enable, edit) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, dialog ) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) - form.addRow(buttons) + layout.addWidget(buttons, len(rows), 0, 1, 2) + return dialog, bounds + + @SafeSlot() + def _open_custom_range_dialog(self): + """One dialog with both bounds - replaces the former nested calendar dialogs.""" + dialog, bounds = self._build_custom_range_dialog() if dialog.exec() == QDialog.DialogCode.Accepted: - values = {} - for bound, (enable, edit) in bounds.items(): - values[bound] = edit.dateTime() if enable.isChecked() else None - self.timestamp_update.emit(TimestampUpdate(values[bound], bound)) - start, end = values["start"], values["end"] - if start and end: - text = f"{start.toString('HH:mm')} - {end.toString('HH:mm')}" - elif start: - text = f">= {start.toString('HH:mm:ss')}" - elif end: - text = f"<= {end.toString('HH:mm:ss')}" - else: - text = "All time" - self.timerange_button.setText(text) + self._apply_custom_range(bounds) dialog.deleteLater() + def _apply_custom_range(self, bounds: dict[str, tuple[QCheckBox, QDateTimeEdit]]): + start_enable, start_edit = bounds["start"] + end_enable, end_edit = bounds["end"] + self._set_time_range( + start_edit.dateTime() if start_enable.isChecked() else None, + end_edit.dateTime() if end_enable.isChecked() else None, + ) + # ---------------------------------------------------------------- search @SafeSlot(str) diff --git a/tests/unit_tests/test_logpanel.py b/tests/unit_tests/test_logpanel.py index 6d09ef37..226500bf 100644 --- a/tests/unit_tests/test_logpanel.py +++ b/tests/unit_tests/test_logpanel.py @@ -462,6 +462,34 @@ def test_log_panel_set_level_with_unlisted_level_keeps_filter(qtbot, log_panel: assert log_panel._toolbar.filter_level_dropdown.currentIndex() == 0 + +def test_log_panel_custom_range_dialog_prefills_active_bounds(qtbot, log_panel: LogPanel): + toolbar = log_panel._toolbar + toolbar._apply_time_preset("Last 5 min", 300) + qtbot.waitUntil(lambda: log_panel._proxy._ts_start is not None, timeout=200) + dialog, bounds = toolbar._build_custom_range_dialog() + try: + start_enable, start_edit = bounds["start"] + end_enable, end_edit = bounds["end"] + # prefilled from the active preset instead of stale defaults + assert start_enable.isChecked() + assert start_edit.dateTime() == toolbar._active_start + assert not end_enable.isChecked() + # applying with an added end bound routes both bounds to the proxy + end_enable.setChecked(True) + end_edit.setDateTime(toolbar._active_start.addSecs(60)) + toolbar._apply_custom_range(bounds) + qtbot.waitUntil(lambda: log_panel._proxy._ts_end is not None, timeout=200) + assert "-" in toolbar.timerange_button.text() + # and 'All time' clears everything again + toolbar._apply_time_preset("All time", None) + qtbot.waitUntil(lambda: log_panel._proxy._ts_start is None, timeout=200) + assert log_panel._proxy._ts_end is None + assert toolbar.timerange_button.text() == "All time" + finally: + dialog.deleteLater() + + def test_log_panel_colors_follow_theme(qtbot, log_panel: LogPanel): info_index = log_panel._model.index(1, 0) success_index = log_panel._model.index(2, 0)