Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions src/glider/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
QDialog,
QDockWidget,
QFileDialog,
QFrame,
QHBoxLayout,
QLabel,
QMainWindow,
QMessageBox,
Expand Down Expand Up @@ -105,6 +107,12 @@ def __init__(
# Toolbar status (initialised here so _on_core_state_change can test)
self._toolbar_status: QLabel | None = None

# Status bar widgets
self._conn_dot: QLabel | None = None
self._conn_label: QLabel | None = None
self._state_label: QLabel | None = None
self._stats_label: QLabel | None = None

# Zone configuration
self._zone_config = ZoneConfiguration()

Expand Down Expand Up @@ -399,6 +407,13 @@ def _setup_menu(self) -> None:

menubar = self.menuBar()

branding = QLabel("GLIDER")
branding.setStyleSheet(
f"font-weight: 600; color: {colors.ACCENT}; font-size: 13px; "
f"letter-spacing: 0.5px; padding: 0 12px 0 4px;"
)
menubar.setCornerWidget(branding, Qt.Corner.TopLeftCorner)

# File menu
file_menu = menubar.addMenu("&File")

Expand Down Expand Up @@ -617,13 +632,46 @@ def _setup_toolbar(self) -> None:
toolbar.addWidget(self._toolbar_status)

def _setup_status_bar(self) -> None:
"""Set up the status bar."""
"""Set up the status bar with connection, state, and stats."""
if self._view_manager.is_runner_mode:
return

status_bar = QStatusBar()
status_bar.setSizeGripEnabled(False)
self.setStatusBar(status_bar)
status_bar.showMessage("Ready")

# Connection indicator (left)
conn_widget = QWidget()
conn_layout = QHBoxLayout(conn_widget)
conn_layout.setContentsMargins(4, 0, 4, 0)
conn_layout.setSpacing(4)

self._conn_dot = QLabel("\u2022")
self._conn_dot.setStyleSheet(f"color: {colors.ERROR}; font-size: 16px;")
conn_layout.addWidget(self._conn_dot)

self._conn_label = QLabel("No board")
self._conn_label.setProperty("textRole", "muted")
conn_layout.addWidget(self._conn_label)

status_bar.addWidget(conn_widget)

# Separator
sep = QFrame()
sep.setFrameShape(QFrame.Shape.VLine)
sep.setStyleSheet(f"color: {colors.BORDER};")
sep.setFixedHeight(14)
status_bar.addWidget(sep)

# State label
self._state_label = QLabel("State: IDLE")
self._state_label.setProperty("textRole", "muted")
status_bar.addWidget(self._state_label)

# Stats (right-aligned)
self._stats_label = QLabel("")
self._stats_label.setProperty("textRole", "disabled")
status_bar.addPermanentWidget(self._stats_label)

def _show_status_message(self, message: str, timeout: int = 0) -> None:
"""Show a status bar message if not in runner mode."""
Expand Down Expand Up @@ -666,6 +714,9 @@ def _on_core_state_change(self, state) -> None:

self._show_status_message(f"State: {state_name}")

if self._state_label is not None:
self._state_label.setText(f"State: {state_name}")

@pyqtSlot(str, object)
def _on_core_error(self, source: str, error: Exception) -> None:
"""Handle core errors."""
Expand All @@ -691,6 +742,32 @@ def _on_hardware_connection_change(self, board_id: str, state: BoardConnectionSt
self._run_async(self._core.pause())
self._show_hardware_disconnection_dialog(board_id, state)

self._update_connection_status()

def _update_connection_status(self) -> None:
"""Update the status bar connection indicator."""
if self._conn_dot is None:
return
boards = self._core.hardware_manager.boards
connected = any(
getattr(b, "_connected", False) or getattr(b, "connected", False)
for b in boards.values()
)
if connected:
for board_id, board in boards.items():
if getattr(board, "_connected", False) or getattr(board, "connected", False):
name = getattr(board, "name", board_id)
self._conn_dot.setStyleSheet(f"color: {colors.SUCCESS}; font-size: 16px;")
self._conn_label.setText(f"{name} \u2014 Connected")
return
self._conn_dot.setStyleSheet(f"color: {colors.ERROR}; font-size: 16px;")
self._conn_label.setText("No board")

def update_status_stats(self, node_count: int, connection_count: int) -> None:
"""Update the node/connection count in the status bar."""
if self._stats_label is not None:
self._stats_label.setText(f"{node_count} nodes \u2022 {connection_count} connections")

def _show_hardware_disconnection_dialog(
self, board_id: str, state: BoardConnectionState
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/glider/gui/panels/camera_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def _setup_ui(self) -> None:
layout.addWidget(cv_section_label)

cv_layout = QVBoxLayout()
cv_layout.setSpacing(4)
cv_layout.setSpacing(10)

self._cv_enabled_cb = QCheckBox("Computer Vision")
self._cv_enabled_cb.toggled.connect(self._on_cv_toggle)
Expand Down
23 changes: 11 additions & 12 deletions src/glider/gui/panels/device_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,31 +155,30 @@ def _setup_ui(self):
)
input_group_layout.addWidget(self._input_value_label)

input_row = QHBoxLayout()
input_row.setSpacing(4)
self._read_btn = QPushButton("Read")
self._read_btn.setMinimumHeight(32)
self._read_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._read_btn.clicked.connect(self._read_input_once)
input_row.addWidget(self._read_btn)
input_group_layout.addWidget(self._read_btn)

poll_row = QHBoxLayout()
poll_row.setSpacing(8)
self._continuous_checkbox = QCheckBox("Auto")
self._continuous_checkbox.setMinimumWidth(35)
self._continuous_checkbox.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self._continuous_checkbox.stateChanged.connect(self._on_continuous_changed)
input_row.addWidget(self._continuous_checkbox)
poll_row.addWidget(self._continuous_checkbox)

poll_label = QLabel("Interval:")
poll_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
poll_row.addWidget(poll_label)

self._poll_spinbox = QSpinBox()
self._poll_spinbox.setRange(50, 5000)
self._poll_spinbox.setValue(100)
self._poll_spinbox.setSuffix("ms")
self._poll_spinbox.setMinimumWidth(75)
self._poll_spinbox.setSuffix(" ms")
self._poll_spinbox.setMinimumHeight(28)
self._poll_spinbox.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self._poll_spinbox.valueChanged.connect(self._on_poll_interval_changed)
input_row.addWidget(self._poll_spinbox)
poll_row.addWidget(self._poll_spinbox)

input_group_layout.addLayout(input_row)
input_group_layout.addLayout(poll_row)
self._input_group = input_group
self._control_layout.addWidget(input_group)

Expand Down
36 changes: 35 additions & 1 deletion src/glider/gui/panels/node_editor_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
QDoubleSpinBox,
QFileDialog,
QFormLayout,
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
Expand Down Expand Up @@ -401,6 +402,21 @@ def _on_connection_deleted(self, connection_id: str) -> None:

self.flow_functions_changed.emit()

@staticmethod
def _add_section_header(layout: QFormLayout, text: str) -> None:
"""Add an uppercase section header label spanning the full row."""
header = QLabel(text)
header.setProperty("class", "props-section-header")
layout.addRow(header)

@staticmethod
def _add_divider(layout: QFormLayout) -> None:
"""Add a horizontal divider line spanning the full row."""
line = QFrame()
line.setFrameShape(QFrame.Shape.HLine)
line.setProperty("class", "props-divider")
layout.addRow(line)

def _update_properties_panel(self, node_id: str) -> None:
"""Update the properties panel for the selected node."""
if self._properties_dock is None:
Expand All @@ -417,8 +433,11 @@ def _update_properties_panel(self, node_id: str) -> None:

props_widget = QWidget()
props_layout = QFormLayout(props_widget)
props_layout.setContentsMargins(8, 8, 8, 8)
props_layout.setContentsMargins(12, 12, 12, 12)
props_layout.setVerticalSpacing(6)

# -- Node Info section --
self._add_section_header(props_layout, "NODE INFO")
props_layout.addRow("ID:", QLabel(node_id))
props_layout.addRow("Type:", QLabel(node_item.node_type))

Expand All @@ -427,8 +446,11 @@ def _update_properties_panel(self, node_id: str) -> None:
else:
node_type = node_item.node_type.replace(" ", "")

self._add_divider(props_layout)

# Add device selector for I/O nodes
if node_type in ["Output", "Input", "WaitForInput", "MotorGovernor"]:
self._add_section_header(props_layout, "DEVICE")
device_combo = QComboBox()
device_combo.addItem("-- Select Device --", None)
current_device_id = node_config.device_id if node_config else None
Expand All @@ -450,6 +472,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow("Device:", device_combo)

elif node_type == "Delay":
self._add_section_header(props_layout, "CONFIGURATION")
duration_spin = QSpinBox()
duration_spin.setRange(0, 3600)
saved_duration = 1
Expand All @@ -463,6 +486,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow("Duration:", duration_spin)

elif node_type == "StartFunction":
self._add_section_header(props_layout, "FUNCTION")
name_edit = QLineEdit()
name_edit.setPlaceholderText("Enter function name")
saved_name = "MyFunction"
Expand All @@ -481,6 +505,8 @@ def _update_properties_panel(self, node_id: str) -> None:

# Value control for Output node
if node_type == "Output":
self._add_divider(props_layout)
self._add_section_header(props_layout, "VALUE")
bound_device_type = None
if node_config and node_config.device_id:
bound_device = self._hardware_manager.get_device(node_config.device_id)
Expand Down Expand Up @@ -526,6 +552,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow("Value:", value_widget)

elif node_type == "MotorGovernor":
self._add_section_header(props_layout, "ACTION")
action_combo = QComboBox()
action_combo.addItem("Move Up", "up")
action_combo.addItem("Move Down", "down")
Expand All @@ -546,6 +573,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow("Action:", action_combo)

elif node_type == "Loop":
self._add_section_header(props_layout, "LOOP SETTINGS")
count_spin = QSpinBox()
count_spin.setRange(0, 10000)
count_spin.setSpecialValueText("Infinite")
Expand Down Expand Up @@ -573,6 +601,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow("Delay:", delay_spin)

elif node_type == "WaitForInput":
self._add_section_header(props_layout, "INPUT SETTINGS")
mode_combo = QComboBox()
mode_combo.addItem("Digital (Rising Edge)", "digital")
mode_combo.addItem("Analog (Threshold)", "analog")
Expand Down Expand Up @@ -639,6 +668,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow(info_label)

elif node_type == "AnalogRead":
self._add_section_header(props_layout, "ANALOG SETTINGS")
continuous_check = QCheckBox("Enable continuous reading")
saved_continuous = False
if node_config and node_config.state:
Expand Down Expand Up @@ -710,6 +740,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow(info_label)

elif node_type in ("CustomDevice", "CustomDeviceAction"):
self._add_section_header(props_layout, "CUSTOM DEVICE")
definition_id = None
if node_config and node_config.state:
definition_id = node_config.state.get("definition_id")
Expand Down Expand Up @@ -810,6 +841,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow(QLabel("(Custom device not found)"))

elif node_type == "FlowFunctionCall":
self._add_section_header(props_layout, "FLOW FUNCTION")
definition_id = None
if node_config and node_config.state:
definition_id = node_config.state.get("definition_id")
Expand All @@ -833,6 +865,7 @@ def _update_properties_panel(self, node_id: str) -> None:
props_layout.addRow(QLabel("(Flow function not found)"))

elif node_type == "AudioPlayback":
self._add_section_header(props_layout, "AUDIO")
file_edit = QLineEdit()
file_edit.setReadOnly(True)
file_edit.setPlaceholderText("No file selected")
Expand Down Expand Up @@ -913,6 +946,7 @@ def on_audio_device_changed(idx, nid=node_id, combo=device_combo):
props_layout.addRow("Volume:", volume_spin)

elif node_type == "VideoPlayback":
self._add_section_header(props_layout, "VIDEO")
file_edit = QLineEdit()
file_edit.setReadOnly(True)
file_edit.setPlaceholderText("No file selected")
Expand Down
Loading
Loading