from gi.repository import GLib, Gdk from fabric.widgets.label import Label from fabric.widgets.box import Box from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.button import Button from services.battery import Battery as BatteryService import utils.icons as icons import config.data as data class Battery(Box): def __init__(self, **kwargs): orientation = "v" if data.VERTICAL else "h" super().__init__( name="battery", spacing=0, orientation=orientation, visible=True, **kwargs ) # Initialize battery service self._battery = BatteryService() self._battery.changed.connect(self.update_battery) # Create the battery icon and circular progress self.icon = Label(name="battery-icon", markup=icons.battery) self.circle = CircularProgressBar( name="battery-circle", value=0, size=26, line_width=2, start_angle=150, end_angle=390, style_classes="battery", child=self.icon, ) # Create button to hold the circle self.circle_button = Button( name="battery-button", child=self.circle, ) # Add event handling for hover effects self.circle_button.add_events( Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.BUTTON_PRESS_MASK ) self.circle_button.connect("enter-notify-event", self.on_button_enter) self.circle_button.connect("leave-notify-event", self.on_button_leave) self.circle_button.connect("button-press-event", self.on_button_press) # Create percentage label self.level = Label( name="battery-level", style_classes="battery", label="0%", ) # Create the container box self.battery_box = Box( name="battery-box", orientation=orientation, spacing=0, children=[self.circle_button], ) # Only add level label in horizontal mode if not data.VERTICAL: self.battery_box.add(self.level) # Enable tooltips self.circle_button.set_has_tooltip(True) self.battery_box.set_has_tooltip(True) self.level.set_has_tooltip(True) # Connect tooltip signals self.circle_button.connect("query-tooltip", self.on_query_tooltip) self.battery_box.connect("query-tooltip", self.on_query_tooltip) self.level.connect("query-tooltip", self.on_query_tooltip) # Add the battery box to self self.add(self.battery_box) # Initial update GLib.idle_add(self.update_battery) def on_button_enter(self, widget, event): """Handle mouse enter event""" window = widget.get_window() if window: window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) def on_button_leave(self, widget, event): """Handle mouse leave event""" window = widget.get_window() if window: window.set_cursor(None) def on_button_press(self, widget, event): """Handle button press event - toggle power profiles""" if event.button == 1: # Left click try: if ( not hasattr(self._battery, "_profile_proxy") or not self._battery._profile_proxy ): return False current_profile = self._battery._profile_proxy.ActiveProfile # Get available profiles from D-Bus property profiles = self._battery._profile_proxy.Profiles profile_names = [] # Extract profile names from the D-Bus structure for profile in profiles: if isinstance(profile, dict) and "Profile" in profile: profile_names.append(profile["Profile"]) elif hasattr(profile, "Profile"): profile_names.append(profile.Profile) elif isinstance(profile, str): profile_names.append(profile) if profile_names and current_profile in profile_names: next_index = (profile_names.index(current_profile) + 1) % len( profile_names ) next_profile = profile_names[next_index] # Set profile directly on the D-Bus proxy self._battery._profile_proxy.ActiveProfile = next_profile else: return False except Exception: return False return True return False def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): """Handle tooltip query""" tooltip.set_markup(self.get_tooltip_text()) return True def get_tooltip_text(self): """Get formatted tooltip text in bullet points""" if not self._battery.is_present: return "No battery detected" percentage = self._battery.percentage state = self._battery.state # Get capacity as integer by converting from float capacity = int(float(self._battery.capacity.rstrip("%"))) tooltip_points = [] # Status and percentage if state == "FULLY_CHARGED": status = f"{icons.bat_full} Fully Charged" elif state == "CHARGING": status = f"{icons.bat_charging} Charging" elif percentage <= 15 and state == "DISCHARGING": status = f"{icons.bat_low} Low Battery" elif state == "DISCHARGING": status = f"{icons.bat_discharging} Discharging" else: status = "Battery" tooltip_points.append(f"• Status: {status}") # Battery level and capacity tooltip_points.append(f"• Level: {int(percentage)}%") tooltip_points.append(f"• Battery Health: {capacity}%") # Add power profile if available power_profile = self._battery.power_profile if power_profile: # Add icon based on profile if power_profile == "power-saver": profile_icon = icons.power_saving elif power_profile == "performance": profile_icon = icons.power_performance else: # balanced profile_icon = icons.power_balanced tooltip_points.append(f"• Profile: {profile_icon} {power_profile}") # Add temperature if available temp = self._battery.temperature if temp != "N/A": tooltip_points.append(f"• Temperature: {temp}") # Add time estimates if state == "CHARGING": time_to_full = self._battery.time_to_full if time_to_full: tooltip_points.append(f"• Time until full: {time_to_full}") elif state == "DISCHARGING": time_to_empty = self._battery.time_to_empty if time_to_empty: tooltip_points.append(f"• Time remaining: {time_to_empty}") return "\n".join(tooltip_points) def update_battery(self, *args): """Update battery status and UI""" if not self._battery.is_present: self.set_visible(False) return True percentage = self._battery.percentage state = self._battery.state charging = state in ["CHARGING", "FULLY_CHARGED"] power_profile = self._battery.power_profile # Update circle progress self.circle.set_value(percentage / 100.0) # Only show percentage label if not at 100% if percentage < 100: self.level.set_label(f"{int(percentage)}%") self.level.set_visible(True) else: self.level.set_visible(False) # Remove any existing profile and discharging classes self.circle.remove_style_class("power-saver") self.circle.remove_style_class("performance") self.circle.remove_style_class("balanced") self.circle.remove_style_class("discharging") self.circle.remove_style_class("discharging-low") self.circle.remove_style_class("discharging-critical") self.icon.remove_style_class("discharging") self.icon.remove_style_class("discharging-low") self.icon.remove_style_class("discharging-critical") # Apply power profile styling and icons if power_profile: self.circle.add_style_class(power_profile) if power_profile == "power-saver": self.icon.set_markup(icons.power_saving) elif power_profile == "performance": self.icon.set_markup(icons.power_performance) else: # balanced # In balanced mode, show discharging icons when discharging if state == "DISCHARGING": # Show different discharging icons and apply styling based on battery level if percentage <= 15: self.icon.set_markup(icons.bat_low) self.circle.add_style_class("discharging-critical") self.icon.add_style_class("discharging-critical") elif percentage <= 30: self.icon.set_markup(icons.alert) self.circle.add_style_class("discharging-low") self.icon.add_style_class("discharging-low") else: self.icon.set_markup(icons.bat_discharging) self.circle.add_style_class("discharging") self.icon.add_style_class("discharging") elif state == "CHARGING": self.icon.set_markup(icons.charging) else: self.icon.set_markup(icons.battery) else: # No power profile - show basic battery state icons if state == "FULLY_CHARGED": self.icon.set_markup(icons.battery) elif state == "CHARGING": self.icon.set_markup(icons.charging) elif state == "DISCHARGING": self.icon.set_markup(icons.discharging) else: self.icon.set_markup(icons.battery) # Apply alert styling if battery is low AND not charging if percentage <= 15 and not charging: self.icon.add_style_class("alert") self.circle.add_style_class("alert") else: self.icon.remove_style_class("alert") self.circle.remove_style_class("alert") self.set_visible(True) return True