From ec65d8ead4e6ca688d0bc1594cb61de9fca88db9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 5 Jul 2026 23:05:49 +0545 Subject: [PATCH] feat(src/window/panel/panel): add NumLockIndicator and ChargingIndicator, improve battery service initialization --- src/window/panel/components/indicators.py | 4 +- src/window/panel/notch/__init__.py | 8 +- src/window/panel/notch/indicators.py | 163 ++++++++++++++++++++-- src/window/panel/notch/notch.py | 43 ++++-- 4 files changed, 193 insertions(+), 25 deletions(-) diff --git a/src/window/panel/components/indicators.py b/src/window/panel/components/indicators.py index 0697676..95882ee 100644 --- a/src/window/panel/components/indicators.py +++ b/src/window/panel/components/indicators.py @@ -1,10 +1,10 @@ -from services.bluetooth import BluetoothClient from fabric.utils import logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.label import Label from services.battery import Battery +from services.bluetooth import BluetoothClient from services.network import NetworkClient from shared.window.applet_window import AppletWindow from shared.window.battery_widget import BatteryControl @@ -320,7 +320,7 @@ class BatteryIndicator(Box): super().__init__(name="battery-indicator", orientation="h", **kwargs) self.show_window = show_window - self.battery_service = Battery() + self.battery_service = Battery.get_initial() self.battery_icon = svg_file("battery/battery-100.svg", size=23) diff --git a/src/window/panel/notch/__init__.py b/src/window/panel/notch/__init__.py index d333c11..dc4f686 100644 --- a/src/window/panel/notch/__init__.py +++ b/src/window/panel/notch/__init__.py @@ -1,4 +1,8 @@ from window.panel.notch.notch import Notch -from window.panel.notch.indicators import CapsLockIndicator, KeyboardLayoutIndicator +from window.panel.notch.indicators import ( + CapsLockIndicator, + KeyboardLayoutIndicator, + NumLockIndicator, +) -__all__ = ["Notch", "CapsLockIndicator", "KeyboardLayoutIndicator"] +__all__ = ["Notch", "CapsLockIndicator", "KeyboardLayoutIndicator", "NumLockIndicator"] diff --git a/src/window/panel/notch/indicators.py b/src/window/panel/notch/indicators.py index 50e728f..3a542a3 100644 --- a/src/window/panel/notch/indicators.py +++ b/src/window/panel/notch/indicators.py @@ -3,11 +3,13 @@ from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label +from services.battery import Battery from services.capslock import CapsLock from services.keyboard_layout import KeyboardLayout +from services.numlock import NumLock from utils.utils import svg_file -DISPLAY_MS = 2000 # how long each indicator stays visible +DISPLAY_MS = 2000 # how long each indicator stays visible (ms) class KeyboardLayoutIndicator(Box): @@ -19,7 +21,6 @@ class KeyboardLayoutIndicator(Box): v_align="center", **kwargs, ) - self._show_cb = show_cb self._hide_cb = hide_cb self._hide_timer_id = 0 @@ -75,6 +76,9 @@ class KeyboardLayoutIndicator(Box): super().destroy() +# ── CapsLock ─────────────────────────────────────────────────────────────────── + + class CapsLockIndicator(Box): def __init__(self, show_cb, hide_cb, **kwargs): super().__init__( @@ -84,7 +88,6 @@ class CapsLockIndicator(Box): v_align="center", **kwargs, ) - self._show_cb = show_cb self._hide_cb = hide_cb self._hide_timer_id = 0 @@ -99,7 +102,7 @@ class CapsLockIndicator(Box): ) self._label = Label( name="notch-indicator-label", - label="On", + label="On" if self._caps.is_on else "Off", v_align="center", ) @@ -112,15 +115,11 @@ class CapsLockIndicator(Box): ) ) - # Accessing is_on triggers _ensure_monitoring_started() inside the service — - # without this the 500ms LED poll never begins and state_changed never fires. - _ = self._caps.is_on - self._caps.connect("state_changed", self._on_state_changed) def _on_state_changed(self, _service, is_on: bool): - if not is_on: - return + # Show for BOTH On and Off + self._label.set_label("On" if is_on else "Off") self._show_cb(self) self._schedule_hide() @@ -143,3 +142,147 @@ class CapsLockIndicator(Box): except Exception as e: logger.warning(f"[NotchIndicators] caps disconnect: {e}") super().destroy() + + +class NumLockIndicator(Box): + def __init__(self, show_cb, hide_cb, **kwargs): + super().__init__( + name="notch-numlock", + orientation="h", + h_expand=True, + v_align="center", + **kwargs, + ) + self._show_cb = show_cb + self._hide_cb = hide_cb + self._hide_timer_id = 0 + + self._num = NumLock.get_initial() + + self._icon = svg_file( + "misc/num-lock.svg", + size=16, + name="notch-indicator-icon", + v_align="center", + ) + self._label = Label( + name="notch-indicator-label", + label="On" if self._num.is_on else "Off", + v_align="center", + ) + + self.add( + CenterBox( + orientation="h", + h_expand=True, + start_children=self._icon, + end_children=self._label, + ) + ) + + # is_on access also starts the 500ms LED poll inside the service + self._num.connect("state_changed", self._on_state_changed) + + def _on_state_changed(self, _service, is_on: bool): + # Show for BOTH On and Off + self._label.set_label("On" if is_on else "Off") + self._show_cb(self) + self._schedule_hide() + + def _schedule_hide(self): + if self._hide_timer_id: + GLib.source_remove(self._hide_timer_id) + self._hide_timer_id = GLib.timeout_add(DISPLAY_MS, self._do_hide) + + def _do_hide(self): + self._hide_timer_id = 0 + self._hide_cb() + return False + + def destroy(self): + if self._hide_timer_id: + GLib.source_remove(self._hide_timer_id) + self._hide_timer_id = 0 + try: + self._num.disconnect_by_func(self._on_state_changed) + except Exception as e: + logger.warning(f"[NotchIndicators] numlock disconnect: {e}") + super().destroy() + + +class ChargingIndicator(Box): + def __init__(self, show_cb, hide_cb, **kwargs): + super().__init__( + name="notch-charging", + orientation="h", + h_expand=True, + v_align="center", + **kwargs, + ) + self._show_cb = show_cb + self._hide_cb = hide_cb + self._hide_timer_id = 0 + + self._battery = Battery.get_initial() + + self._label = Label( + name="notch-indicator-label", + label="Charging", + v_align="center", + ) + self._icon = svg_file( + "notch/charing.svg", + size=18, + name="notch-indicator-icon", + v_align="center", + ) + + self.add( + CenterBox( + orientation="h", + h_expand=True, + start_children=self._label, + end_children=self._icon, + ) + ) + + self._battery.connect("changed", self._on_battery_changed) + + GLib.timeout_add(500, self._initial_check) + + def _is_charger_connected(self) -> bool: + return self._battery.charging or self._battery.charged + + def _initial_check(self): + self._last_charger_state = self._is_charger_connected() + if self._last_charger_state: + self._show_cb(self) + self._schedule_hide() + return False # one-shot + + def _on_battery_changed(self, _battery): + connected = self._is_charger_connected() + if connected and not self._last_charger_state: + self._show_cb(self) + self._schedule_hide() + self._last_charger_state = connected + + def _schedule_hide(self): + if self._hide_timer_id: + GLib.source_remove(self._hide_timer_id) + self._hide_timer_id = GLib.timeout_add(DISPLAY_MS, self._do_hide) + + def _do_hide(self): + self._hide_timer_id = 0 + self._hide_cb() + return False + + def destroy(self): + if self._hide_timer_id: + GLib.source_remove(self._hide_timer_id) + self._hide_timer_id = 0 + try: + self._battery.disconnect_by_func(self._on_battery_changed) + except Exception as e: + logger.warning(f"[NotchIndicators] charging disconnect: {e}") + super().destroy() diff --git a/src/window/panel/notch/notch.py b/src/window/panel/notch/notch.py index d956570..7b693a4 100644 --- a/src/window/panel/notch/notch.py +++ b/src/window/panel/notch/notch.py @@ -6,7 +6,12 @@ from fabric.widgets.stack import Stack from services.screencapture import screen_capture_service from window.panel.notch.recording import RecordingIndicator from window.panel.notch.player import NotchPlayer -from window.panel.notch.indicators import CapsLockIndicator, KeyboardLayoutIndicator +from window.panel.notch.indicators import ( + CapsLockIndicator, + KeyboardLayoutIndicator, + NumLockIndicator, + ChargingIndicator, +) from window.controlcenter.player import get_shared_mpris_manager @@ -35,6 +40,14 @@ class Notch(Box): h_expand=True, ) + self._last_scroll_index = 0 + self._last_scroll_time = 0 + self._last_recording_check = 0 + self._is_active_recording = False + self._music_services: list[str] = [] + # Must be set BEFORE indicator widgets are created + self._transient_active = False + # Transient indicator pages — shown on change, hidden after 2 s self.kbd_indicator = KeyboardLayoutIndicator( show_cb=self._show_transient, @@ -44,6 +57,15 @@ class Notch(Box): show_cb=self._show_transient, hide_cb=self._hide_transient, ) + self.num_indicator = NumLockIndicator( + show_cb=self._show_transient, + hide_cb=self._hide_transient, + ) + # Persistent indicator — now also transient (2s), deferred via idle_add + self.charging_indicator = ChargingIndicator( + show_cb=self._show_transient, + hide_cb=self._hide_transient, + ) self._mpris = get_shared_mpris_manager() self._mpris.connect("new-player", self._on_new_player) @@ -52,14 +74,6 @@ class Notch(Box): screen_capture_service.connect("recording-started", self._on_recording_started) screen_capture_service.connect("recording-stopped", self._on_recording_stopped) - self._last_scroll_index = 0 - self._last_scroll_time = 0 - self._last_recording_check = 0 - self._is_active_recording = False - self._music_services: list[str] = [] - # Track if a transient indicator is currently shown - self._transient_active = False - self.notch_stack = Stack( name="panel-notch-stack", v_expand=True, @@ -71,6 +85,8 @@ class Notch(Box): self.player_widget, self.kbd_indicator, self.caps_indicator, + self.num_indicator, + self.charging_indicator, ], ) self._last_stack_page = 0 @@ -126,7 +142,7 @@ class Notch(Box): self.notch_stack.set_visible_child(widget) def _hide_transient(self): - """Return to the normal notch state after the indicator timer expires.""" + """Return to normal notch state after the indicator timer expires.""" self._transient_active = False self._apply_stack_state() @@ -231,7 +247,10 @@ class Notch(Box): self.notch_stack.set_visible_child(self.idle_widget) return - pages = {1: self.recording_indicator, 2: self.player_widget} + pages = { + 1: self.recording_indicator, + 2: self.player_widget, + } if len(active_pages) == 1: target = active_pages[0] @@ -248,4 +267,6 @@ class Notch(Box): self.player_widget.destroy() self.kbd_indicator.destroy() self.caps_indicator.destroy() + self.num_indicator.destroy() + self.charging_indicator.destroy() super().destroy()