Skip to content

Comments

Feat/status bar#966

Draft
wyzula-jan wants to merge 161 commits intopre_release_v3from
feat/status-bar
Draft

Feat/status bar#966
wyzula-jan wants to merge 161 commits intopre_release_v3from
feat/status-bar

Conversation

@wyzula-jan
Copy link
Contributor

@wyzula-jan wyzula-jan commented Dec 1, 2025

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

  • Added widget and action for toolbar for BL Status
  • New StatusToolBar
  • Headless Singleton Status Broker distributing status updates and newly defined statuses by BEC

How to test

  • Run unit tests
  • Define BL Condition in CLI:
default@bec [3/18] ❯❯ samx_condition = DeviceWithinLimitsCondition(name='samx_limits',title="SamX")
• default@bec [4/18] ❯❯ samx_condition.configure(device="samx",min_limit=-10,max_limit=10,signal="samx")
• default@bec [5/18] ❯❯ bec.beamline_conditions.add(samx_condition)
  • The new pill should show automatically in the GUI Main Window in bottom left
  • try to move the motor outside the limits to see the pill status change and check tooltip

Potential side effects

...

Screenshots / GIFs (if applicable)

image

wyzula-jan and others added 30 commits October 15, 2025 14:07
… settings

wip widget state manager saving loading file logic
@codecov
Copy link

codecov bot commented Dec 1, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 StatusIndicatorWidget and StatusIndicatorAction classes for displaying LED-style status indicators
  • Extends ViewBase with optional status toolbar functionality controlled via show_status parameter
  • 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"):
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
"""

Copilot uses AI. Check for mistakes.

def _apply_state(self, state: str):
palette = self._resolve_accent_colors()
color = getattr(palette, state, None) or getattr(palette, "default", QColor("gray"))
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"))

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +205
"""Toolbar action hosting a LED indicator and status text."""

Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"""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".
"""

Copilot uses AI. Check for mistakes.
*,
id: str | None = None,
title: str | None = None,
show_status: bool = True,
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@wyzula-jan wyzula-jan force-pushed the feat/status-bar branch 4 times, most recently from 09ddcab to 7d795c5 Compare December 5, 2025 15:51
@wyzula-jan wyzula-jan force-pushed the feat/status-bar branch 3 times, most recently from 81c4208 to 81c2a78 Compare December 8, 2025 10:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +25 to +40
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()
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +413 to +414
if hasattr(self, "_app_id_action"):
self._app_id_action.setText(status_message)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if hasattr(self, "_app_id_action"):
self._app_id_action.setText(status_message)
self._app_id_action.setText(status_message)

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +166
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)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
QTimer.singleShot(0, self.refresh_from_broker)

def refresh_from_broker(self) -> None:

Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +33
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)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +135
if self.status_bar is None:
return
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if self.status_bar is None:
return

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +174
known_actions = [
n for n in self.components._components.keys() if n not in ("separator",)
] # direct access used for clean-up
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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",)
]

Copilot uses AI. Check for mistakes.
self.refresh()

@SafeSlot(str, dict)
def on_status_updated(self, name: str, payload: dict): # TODO finish update logic
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
def on_status_updated(self, name: str, payload: dict): # TODO finish update logic
def on_status_updated(self, name: str, payload: dict):

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +266
except Exception:
pass
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
pass
except Exception as e:
logger.exception("Failed to get theme foreground color")

Copilot uses AI. Check for mistakes.
try:
theme.theme_changed.connect(lambda _: self._apply_state(self._state))
self._theme_connected = True
except Exception:
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# Ignore exceptions when connecting theme_changed signal.
# This may happen if the theme object does not support signal connections.

Copilot uses AI. Check for mistakes.
@wyzula-jan wyzula-jan force-pushed the pre_release_v3 branch 7 times, most recently from c28fbf6 to b4ad75a Compare December 19, 2025 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants