diff --git a/modules/dock/components/battery.py b/modules/dock/components/battery.py index eed2a10..c56ce72 100644 --- a/modules/dock/components/battery.py +++ b/modules/dock/components/battery.py @@ -1,391 +1,12 @@ -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.circularprogressbar import CircularProgressBar -from fabric.widgets.eventbox import EventBox +from gi.repository import GLib + from fabric.widgets.label import Label -from fabric.widgets.revealer import Revealer -from gi.repository import Gdk, GLib - -import config.data as data -import utils.icons as icons +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 -from utils.wayland import WaylandWindow as Window - - -class BatteryPopup(Window): - """Popup window that shows battery information and power profile switcher""" - - def __init__(self, dock_component=None, **kwargs): - # Get dock position for proper anchoring - dock_position = data.DOCK_POSITION - popup_anchor = self._get_popup_anchor(dock_position) - margin = self._get_popup_margin(dock_position) - - super().__init__( - name="battery-popup", - layer="top", - anchor=popup_anchor, - margin=margin, - exclusive=False, - keyboard_mode="on-demand", - visible=False, - all_visible=False, - **kwargs, - ) - - self.dock_component = dock_component - - # Get transition type based on dock position - transition_type = self._get_revealer_transition(dock_position) - - # Create main content box - self.content_box = Box( - name="battery-popup-content", - orientation="v", - spacing=12, - style_classes=["battery-popup"], - ) - - # Create battery info section - self.info_box = Box( - name="battery-info", - orientation="v", - spacing=6, - ) - - # Create power profile switcher section - self.profile_box = Box( - name="power-profile-switcher", - orientation="h", - spacing=8, - style_classes=["power-profiles"], - ) - - # Create power profile buttons - # We'll determine the actual profile names dynamically - self.power_saver_btn = Button( - name="power-saver-btn", - child=Label(markup=icons.power_saving,name="bat-icon"), - on_clicked=lambda *_: self._set_power_profile_by_type("power-saver"), - style_classes=["power-profile-btn"], - ) - - self.balanced_btn = Button( - name="balanced-btn", - child=Label(markup=icons.power_balanced,name="bat-icon"), - on_clicked=lambda *_: self._set_power_profile_by_type("balanced"), - style_classes=["power-profile-btn"], - ) - - self.performance_btn = Button( - name="performance-btn", - child=Label(markup=icons.power_performance,name="bat-icon"), - on_clicked=lambda *_: self._set_power_profile_by_type("performance"), - style_classes=["power-profile-btn"], - ) - - # Add profile buttons to profile box - self.profile_box.add(self.power_saver_btn) - self.profile_box.add(self.balanced_btn) - self.profile_box.add(self.performance_btn) - - # Add sections to content box - self.content_box.add(self.info_box) - self.content_box.add(self.profile_box) - - # Create revealer for slide transition - self.revealer = Revealer( - transition_type=transition_type, - transition_duration=300, - child=self.content_box, - reveal_child=False, - ) - - self.add(self.revealer) - - # Connect to escape key to close - self.connect("key-press-event", self.on_key_press) - self.connect("button-press-event", self.on_button_press) - self.connect("enter-notify-event", self.on_enter_notify) - self.connect("leave-notify-event", self.on_leave_notify) - self.set_can_focus(True) - - def _set_power_profile_by_type(self, profile_type): - """Set power profile by type (maps to actual profile names)""" - if self.dock_component and hasattr(self.dock_component, '_battery'): - try: - battery_service = self.dock_component._battery - - # Check if profile proxy is available - if not hasattr(battery_service, '_profile_proxy') or not battery_service._profile_proxy: - print("Power profile proxy not available") - return - - # Get available profiles - profiles = battery_service._profile_proxy.Profiles - available_profiles = [] - for p in profiles: - if isinstance(p, dict) and "Profile" in p: - available_profiles.append(p["Profile"]) - elif hasattr(p, "Profile"): - available_profiles.append(p.Profile) - elif isinstance(p, str): - available_profiles.append(p) - - # Map profile types to actual profile names - profile_mapping = { - "power-saver": ["power-saver", "powersave", "power_saver"], - "balanced": ["balanced", "balance"], - "performance": ["performance", "performance-mode"] - } - - # Find the actual profile name - actual_profile = None - if profile_type in profile_mapping: - for candidate in profile_mapping[profile_type]: - if candidate in available_profiles: - actual_profile = candidate - break - - if actual_profile: - battery_service._profile_proxy.ActiveProfile = actual_profile - self.update_profile_buttons() - self.dock_component.update_battery() - print(f"Successfully set power profile to {actual_profile}") - else: - print(f"No matching profile found for type '{profile_type}' in available profiles: {available_profiles}") - - except Exception as e: - print(f"Error setting power profile: {e}") - - def _set_power_profile(self, profile): - """Set the power profile""" - if self.dock_component and hasattr(self.dock_component, '_battery'): - try: - battery_service = self.dock_component._battery - - # Check if profile proxy is available - if not hasattr(battery_service, '_profile_proxy') or not battery_service._profile_proxy: - print("Power profile proxy not available") - return - - # Get available profiles for debugging - try: - profiles = battery_service._profile_proxy.Profiles - available_profiles = [] - for p in profiles: - if isinstance(p, dict) and "Profile" in p: - available_profiles.append(p["Profile"]) - elif hasattr(p, "Profile"): - available_profiles.append(p.Profile) - elif isinstance(p, str): - available_profiles.append(p) - - print(f"Available profiles: {available_profiles}") - print(f"Trying to set profile: {profile}") - - # Try to set the profile - if profile in available_profiles: - battery_service._profile_proxy.ActiveProfile = profile - self.update_profile_buttons() - # Update the dock component as well - self.dock_component.update_battery() - print(f"Successfully set power profile to {profile}") - else: - print(f"Profile '{profile}' not found in available profiles: {available_profiles}") - - except Exception as e: - print(f"Error accessing profiles: {e}") - - except Exception as e: - print(f"Error setting power profile: {e}") - - def update_content(self): - """Update the popup content with current battery information""" - if not self.dock_component: - return - - # Clear existing info - for child in self.info_box.get_children(): - self.info_box.remove(child) - - battery = self.dock_component._battery - - # Battery percentage and state - percentage_label = Label( - name="battery-percentage", - label=f"{battery.percentage}%", - style_classes=["battery-percentage"], - ) - - state_label = Label( - name="battery-state", - label=battery.state, - style_classes=["battery-state"], - ) - - # Create percentage and state row - percentage_state_box = Box( - name="percentage-state-box", - orientation="h", - spacing=8, - children=[percentage_label, state_label], - ) - - # Battery capacity (health) - capacity_box = Box( - name="capacity-box", - orientation="h", - spacing=8, - children=[ - Label(label="Battery Health:", style_classes=["battery-label"]), - Label(label=battery.capacity, style_classes=["battery-value"]), - ], - ) - - # Time information based on charging state - time_box = None - if battery.state == "CHARGING": - time_to_full = battery.time_to_full - if time_to_full and time_to_full != "0m": - time_box = Box( - name="time-box", - orientation="h", - spacing=8, - children=[ - Label(label="Time to full:", style_classes=["battery-label"]), - Label(label=time_to_full, style_classes=["battery-value"]), - ], - ) - else: # DISCHARGING or other states - time_to_empty = battery.time_to_empty - if time_to_empty and time_to_empty != "0m": - time_box = Box( - name="time-box", - orientation="h", - spacing=8, - children=[ - Label(label="Time remaining:", style_classes=["battery-label"]), - Label(label=time_to_empty, style_classes=["battery-value"]), - ], - ) - - # Power profile information - profile_label_box = Box( - name="profile-label-box", - orientation="h", - spacing=8, - children=[ - Label(label="Power Profile:", style_classes=["battery-label"]), - Label(label=battery.power_profile.title(), style_classes=["battery-value"]), - ], - ) - - # Add all info to the info box - self.info_box.add(percentage_state_box) - self.info_box.add(capacity_box) - if time_box: - self.info_box.add(time_box) - self.info_box.add(profile_label_box) - - # Update profile button states - self.update_profile_buttons() - - def update_profile_buttons(self): - """Update the active state of profile buttons""" - if not self.dock_component or not hasattr(self.dock_component, '_battery'): - return - - current_profile = self.dock_component._battery.power_profile - - # Remove active class from all buttons - self.power_saver_btn.remove_style_class("active") - self.balanced_btn.remove_style_class("active") - self.performance_btn.remove_style_class("active") - - # Add active class to current profile button - if current_profile == "power-saver": - self.power_saver_btn.add_style_class("active") - elif current_profile == "performance": - self.performance_btn.add_style_class("active") - else: # balanced or default - self.balanced_btn.add_style_class("active") - - def on_key_press(self, widget, event): - """Handle key press events""" - if event.keyval == Gdk.KEY_Escape: - self.hide_popup() - return True - return False - - def show_popup(self): - """Show the battery popup""" - self.update_content() - self.set_visible(True) - self.show_all() - # Use GLib.idle_add to ensure the window is shown before revealing - GLib.idle_add(self._reveal_popup) - - def _reveal_popup(self): - """Reveal the popup after window is shown""" - self.revealer.set_reveal_child(True) - self.grab_focus() - return False # Don't repeat - - def _get_popup_anchor(self, dock_position): - """Get popup anchor based on dock position""" - anchor_map = { - "Top": "top", - "Bottom": "bottom right", - "Left": "top left", - "Right": "top right", - } - return anchor_map.get(dock_position, "bottom") - - def _get_popup_margin(self, dock_position): - """Get popup margin based on dock position""" - margin_map = { - "Top": "60px 10px 10px 10px", - "Bottom": "10px 650px 60px 10px", - "Left": "600px 10px 10px 60px", - "Right": "600px 60px 10px 10px", - } - return margin_map.get(dock_position, "10px 10px 60px 10px") - - def _get_revealer_transition(self, dock_position): - """Get revealer transition type based on dock position""" - transition_map = { - "Top": "slide-down", - "Bottom": "slide-up", - "Left": "slide-right", - "Right": "slide-left", - } - return transition_map.get(dock_position, "slide-up") - - def on_button_press(self, widget, event): - """Handle button press events""" - return False - - def on_enter_notify(self, widget, event): - """Handle mouse entering popup""" - # Cancel any pending hide from dock component - if self.dock_component and hasattr(self.dock_component, '_hover_timeout'): - if self.dock_component._hover_timeout: - GLib.source_remove(self.dock_component._hover_timeout) - self.dock_component._hover_timeout = None - return False - - def on_leave_notify(self, widget, event): - """Handle mouse leaving popup""" - # Hide popup when mouse leaves - self.hide_popup() - return False - - def hide_popup(self): - """Hide the battery popup""" - self.revealer.set_reveal_child(False) - # Hide window after transition completes - GLib.timeout_add(350, lambda: self.set_visible(False)) +import utils.icons as icons +import config.data as data class Battery(Box): @@ -395,9 +16,11 @@ class Battery(Box): 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", @@ -410,96 +33,115 @@ class Battery(Box): child=self.icon, ) + # Create button to hold the circle self.circle_button = Button( name="battery-button", child=self.circle, ) - # Create event box for hover detection - self.event_box = EventBox( - events=["enter-notify", "leave-notify"], - child=self.circle_button, - ) - # Connect hover events - self.event_box.connect("enter-notify-event", self.on_enter_notify) - self.event_box.connect("leave-notify-event", self.on_leave_notify) - self.event_box.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK) + # 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.event_box], + 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) - # Pre-create popup for immediate display on hover - self.popup = BatteryPopup(dock_component=self) - - # Hover timeout for debouncing - self._hover_timeout = None - - # Connect to destroy signal for cleanup - self.connect("destroy", self.on_destroy) - + # Initial update GLib.idle_add(self.update_battery) - def on_destroy(self, widget): - """Clean up popup when component is destroyed""" - try: - # Remove hover timeout - if hasattr(self, "_hover_timeout") and self._hover_timeout: - GLib.source_remove(self._hover_timeout) - self._hover_timeout = None - # Clean up popup (popup is always created now) - if self.popup: - try: - self.popup.destroy() - self.popup = None - except Exception: - pass - except Exception: - pass + def on_query_tooltip(self, _widget, _x, _y, _keyboard_mode, tooltip): + """Handle tooltip query""" + tooltip.set_markup(self.get_tooltip_text()) + return True - def on_enter_notify(self, widget, event): - """Handle mouse entering the battery component""" - # Cancel any pending hover timeout - if self._hover_timeout: - GLib.source_remove(self._hover_timeout) - self._hover_timeout = None + def get_tooltip_text(self): + """Get formatted tooltip text in bullet points""" + if not self._battery.is_present: + return "No battery detected" - # Show popup immediately on hover (popup is pre-created) - if not self.popup.get_visible(): - self.popup.show_popup() - return False + 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 = [] - def on_leave_notify(self, widget, event): - """Handle mouse leaving the battery component""" - # Hide popup with a small delay to prevent flickering - self._hover_timeout = GLib.timeout_add(100, self._hide_popup_delayed) - return False + # 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 _hide_popup_delayed(self): - """Hide popup after delay""" - if self.popup and self.popup.get_visible(): - self.popup.hide_popup() - self._hover_timeout = None - return False # Don't repeat timeout - def update_battery(self, *args): + """Update battery status and UI""" if not self._battery.is_present: self.set_visible(False) return True @@ -507,29 +149,20 @@ class Battery(Box): 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) - - self.icon.remove_style_class("discharging") - self.icon.remove_style_class("discharging-low") - self.icon.remove_style_class("discharging-critical") - - 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") @@ -537,9 +170,18 @@ class Battery(Box): self.icon.remove_style_class("alert") self.circle.remove_style_class("alert") - # Only show if enabled in configuration - if data.DOCK_COMPONENTS_VISIBILITY.get("battery", True): - self.set_visible(True) - else: - self.set_visible(False) + # Update icon based on battery state if not in power profile mode + if not power_profile: + if state == "FULLY_CHARGED": + self.icon.set_markup(icons.battery) + elif state == "CHARGING": + self.icon.set_markup(icons.charging) + elif percentage <= 15 and state == "DISCHARGING": + self.icon.set_markup(icons.alert) + elif state == "DISCHARGING": + self.icon.set_markup(icons.discharging) + else: + self.icon.set_markup(icons.battery) + + self.set_visible(True) return True