Conversation
…e widget recursively
… settings wip widget state manager saving loading file logic
…init to prevent segfault
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR adds a status bar feature to views with LED-style indicators that can display different states using color-coded visual feedback. The feature enables views to show status information (e.g., "Ready", "Processing", "Error") with visual indicators that integrate with the application's theme system.
Key changes:
- Introduces
StatusIndicatorWidgetandStatusIndicatorActionclasses for displaying LED-style status indicators - Extends
ViewBasewith optional status toolbar functionality controlled viashow_statusparameter - Exposes the QApplication instance in the Jupyter console for theme/status testing
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| bec_widgets/utils/toolbars/actions.py | Adds StatusIndicatorWidget (visual LED+label) and StatusIndicatorAction (toolbar integration) with theme-aware color resolution |
| bec_widgets/applications/views/view.py | Extends ViewBase with status toolbar initialization and management methods (add_status_item, set_status, etc.) |
| bec_widgets/examples/jupyter_console/jupyter_console_window.py | Exposes QApplication instance to console for easier theme and status testing |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
| class StatusIndicatorWidget(QWidget): | ||
| """Small LED + label widget that reflects a view's status.""" | ||
|
|
||
| def __init__(self, parent=None, text: str = "Ready", state: str = "default"): |
There was a problem hiding this comment.
The state parameter lacks documentation about what valid values are expected. Consider adding a docstring that documents the expected state values (e.g., "default", "success", "error", "warning") or the mechanism by which states map to colors via AccentColors.
| def __init__(self, parent=None, text: str = "Ready", state: str = "default"): | |
| def __init__(self, parent=None, text: str = "Ready", state: str = "default"): | |
| """ | |
| Initialize the StatusIndicatorWidget. | |
| Args: | |
| parent: The parent widget. | |
| text (str): The label text to display next to the LED. | |
| state (str): The status state, which determines the LED color. | |
| Valid values are typically "default", "success", "error", "warning", | |
| or any string key that maps to a color in AccentColors. | |
| The mapping of state values to colors is handled via AccentColors. | |
| """ |
|
|
||
| def _apply_state(self, state: str): | ||
| palette = self._resolve_accent_colors() | ||
| color = getattr(palette, state, None) or getattr(palette, "default", QColor("gray")) |
There was a problem hiding this comment.
The use of getattr(palette, state, None) or getattr(palette, "default", QColor("gray")) could silently mask issues if an invalid state is provided. Consider logging a warning when a state is not found in the palette, or raising an exception for invalid states during development to make debugging easier.
| color = getattr(palette, state, None) or getattr(palette, "default", QColor("gray")) | |
| color = getattr(palette, state, None) | |
| if color is None: | |
| logger.warning( | |
| f"StatusIndicatorWidget: Invalid state '{state}' for text '{self._text}'. " | |
| "Falling back to default color." | |
| ) | |
| color = getattr(palette, "default", QColor("gray")) |
| """Toolbar action hosting a LED indicator and status text.""" | ||
|
|
There was a problem hiding this comment.
The state parameter lacks documentation about what valid values are expected. Consider documenting the expected state values or referencing how they map to colors via AccentColors.
| """Toolbar action hosting a LED indicator and status text.""" | |
| """ | |
| Toolbar action hosting a LED indicator and status text. | |
| Args: | |
| text (str, optional): The status text to display. Defaults to "Ready". | |
| state (str, optional): The state of the indicator, which determines its color. | |
| Valid values are keys from `AccentColors`, such as "default", "success", "warning", "error", etc. | |
| The state value maps to a color via `AccentColors`. | |
| tooltip (str, optional): The tooltip for the action. Defaults to "View status". | |
| """ |
| *, | ||
| id: str | None = None, | ||
| title: str | None = None, | ||
| show_status: bool = True, |
There was a problem hiding this comment.
The new show_status parameter is not documented in the class docstring above (lines 65-74). Consider updating the docstring to describe this parameter and its effect on the view.
09ddcab to
7d795c5
Compare
81c4208 to
81c2a78
Compare
81c2a78 to
d6f5c0e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def __new__(cls, *args, **kwargs): | ||
| if cls._instance is None: | ||
| cls._instance = super().__new__(cls) | ||
| return cls._instance | ||
|
|
||
| def __init__(self, parent=None, gui_id: str | None = None, client=None, **kwargs): | ||
| if self._initialized: | ||
| return | ||
| super().__init__(parent=parent, gui_id=gui_id, client=client, **kwargs) | ||
| self._watched: set[str] = set() | ||
| self.bec_dispatcher.connect_slot( | ||
| self.on_available, MessageEndpoints.available_beamline_conditions() | ||
| ) | ||
|
|
||
| self._initialized = True | ||
| self.refresh_available() |
There was a problem hiding this comment.
The singleton pattern implementation has a potential thread-safety issue. If multiple threads call __new__ simultaneously, they could both see _instance as None and create multiple instances. Similarly, the __init__ check on _initialized could allow multiple initializations. Consider using a lock (e.g., threading.Lock()) to ensure thread-safe singleton creation, or use Python's @functools.lru_cache or threading.local() if appropriate for your use case.
| if hasattr(self, "_app_id_action"): | ||
| self._app_id_action.setText(status_message) |
There was a problem hiding this comment.
The hasattr(self, "_app_id_action") check is unnecessary since _app_id_action is always created in _setup_menu_bar() which is called during initialization. This check adds complexity without clear benefit. Consider either: (1) removing the hasattr check if you're confident the attribute always exists when this method is called, or (2) initializing _app_id_action to None earlier in __init__ to make it a guaranteed instance attribute.
| if hasattr(self, "_app_id_action"): | |
| self._app_id_action.setText(status_message) | |
| self._app_id_action.setText(status_message) |
| if self.allowed_names is None or name in self.allowed_names: | ||
| self.add_status_item(name=name, text=title, state=StatusState.DEFAULT, tooltip=None) | ||
| else: | ||
| # keep hidden but present for context menu toggling | ||
| self.add_status_item(name=name, text=title, state=StatusState.DEFAULT, tooltip=None) |
There was a problem hiding this comment.
The same add_status_item call is duplicated in both branches of the if-else statement (lines 163 and 166). The only difference is whether the action is set to visible afterwards. Consider refactoring this to call add_status_item once before the conditional, then only set visibility inside the else block. This would reduce duplication and make the code easier to maintain.
| if self.allowed_names is None or name in self.allowed_names: | |
| self.add_status_item(name=name, text=title, state=StatusState.DEFAULT, tooltip=None) | |
| else: | |
| # keep hidden but present for context menu toggling | |
| self.add_status_item(name=name, text=title, state=StatusState.DEFAULT, tooltip=None) | |
| self.add_status_item(name=name, text=title, state=StatusState.DEFAULT, tooltip=None) | |
| if not (self.allowed_names is None or name in self.allowed_names): | |
| # keep hidden but present for context menu toggling |
| QTimer.singleShot(0, self.refresh_from_broker) | ||
|
|
||
| def refresh_from_broker(self) -> None: | ||
|
|
There was a problem hiding this comment.
There's an unnecessary blank line at the beginning of the method body. While not a functional issue, it's inconsistent with Python style conventions (PEP 8) which recommend no blank lines immediately after a function definition.
| def __init__(self, parent=None, gui_id: str | None = None, client=None, **kwargs): | ||
| if self._initialized: | ||
| return | ||
| super().__init__(parent=parent, gui_id=gui_id, client=client, **kwargs) |
There was a problem hiding this comment.
The singleton BECStatusBroker combines with QObject which has its own parent-based memory management. Since singletons live for the entire application lifetime, passing a parent to __init__ could lead to unexpected behavior if that parent is deleted. Consider either: (1) not accepting a parent parameter for singletons, or (2) explicitly documenting that the parent should be None or long-lived, or (3) always passing parent=None to the QObject constructor to avoid parent-based lifetime management.
| def __init__(self, parent=None, gui_id: str | None = None, client=None, **kwargs): | |
| if self._initialized: | |
| return | |
| super().__init__(parent=parent, gui_id=gui_id, client=client, **kwargs) | |
| def __init__(self, gui_id: str | None = None, client=None, **kwargs): | |
| if self._initialized: | |
| return | |
| super().__init__(parent=None, gui_id=gui_id, client=client, **kwargs) |
| if self.status_bar is None: | ||
| return |
There was a problem hiding this comment.
The second check if self.status_bar is None: at line 134 is redundant. The setup_status_bar method at line 133 will only create a status bar if one doesn't exist (it has an early return at line 122-123 if self.status_bar is not None). After calling setup_status_bar, the status bar will either exist or will remain None (if setup failed), but the condition can never fail after calling setup when it was None before. Consider removing the redundant check or restructuring the logic for clarity.
| if self.status_bar is None: | |
| return |
| known_actions = [ | ||
| n for n in self.components._components.keys() if n not in ("separator",) | ||
| ] # direct access used for clean-up |
There was a problem hiding this comment.
Direct access to self.components._components (a private attribute indicated by the leading underscore) violates encapsulation. Consider adding a public method to the components API to retrieve all component keys/names, or iterating through a public interface. If direct access is necessary for cleanup, document this exception in a comment explaining why the public API is insufficient.
| known_actions = [ | |
| n for n in self.components._components.keys() if n not in ("separator",) | |
| ] # direct access used for clean-up | |
| # Use public API to get component names instead of accessing private attribute | |
| known_actions = [ | |
| n for n in self.components.get_component_names() if n not in ("separator",) | |
| ] |
| self.refresh() | ||
|
|
||
| @SafeSlot(str, dict) | ||
| def on_status_updated(self, name: str, payload: dict): # TODO finish update logic |
There was a problem hiding this comment.
The TODO comment "finish update logic" suggests incomplete implementation. The logic appears complete - it maps status strings to states, handles title updates, and manages tooltips. Consider removing this TODO comment if the implementation is actually complete, or add specific details about what remains to be implemented.
| def on_status_updated(self, name: str, payload: dict): # TODO finish update logic | |
| def on_status_updated(self, name: str, payload: dict): |
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| except Exception as e: | |
| logger.exception("Failed to get theme foreground color") |
| try: | ||
| theme.theme_changed.connect(lambda _: self._apply_state(self._state)) | ||
| self._theme_connected = True | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| except Exception: | |
| # Ignore exceptions when connecting theme_changed signal. | |
| # This may happen if the theme object does not support signal connections. |
c28fbf6 to
b4ad75a
Compare
b4ad75a to
2ffe269
Compare
Description
Provides a status bar pill like widget based on beamline conditions messages/endpoints introduced in bec-project/bec#676 .
Related Issues
-closes #963
Type of Change
How to test
Potential side effects
...
Screenshots / GIFs (if applicable)