🔧 - Added Notifications component with dock and system tray indicators

This commit is contained in:
sankalp committed 2025-06-27 02:07:58 +05:45
1 parent 54d2f4be0b
commit 18d5470308
10 files changed
+937 -86

No files matched your search

+457
View File
@@ -0,0 +1,457 @@
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
import config.data as data
import utils.icons as icons
from services import notification_service
from modules.notification_popup import NotificationWidget
from utils.wayland import WaylandWindow as Window
class NotificationIndicator(Button):
def __init__(self, **kwargs):
super().__init__(name="button-bar-notifications", **kwargs)
self.notification_service = notification_service
self.notification_count = 0
# Create the notification icon (switches between normal and dots)
self.notification_icon = Label(
name="notification-icon",
markup=icons.notifications
)
# Add the icon directly (no overlay needed)
self.add(self.notification_icon)
# Connect to notification service signals
self.notification_service.connect("notification_count", self.on_notification_count_changed)
self.notification_service.connect("notification-added", self.on_notification_added)
self.notification_service.connect("dnd", self.on_dnd_changed)
# Connect click handler
self.connect("clicked", self.on_clicked)
# Initialize count
self.update_count()
# Popup window for showing notifications
self.popup_window = None
def on_notification_count_changed(self, _service, count):
"""Handle notification count changes."""
print(f"[NotificationIndicator] Count changed to {count}")
self.notification_count = count
self.update_count()
# If popup is open, refresh it to show new notifications
if self.popup_window and self.popup_window.get_visible():
print(f"[NotificationIndicator] Popup is open, refreshing...")
self.popup_window.refresh_popup()
def on_notification_added(self, _service, _notification_id):
"""Handle new notifications being added."""
print(f"[NotificationIndicator] New notification added: {_notification_id}")
# Update count immediately when new notification arrives
self.notification_count = self.notification_service.count
self.update_count()
# If popup is open, refresh it to show new notifications
if self.popup_window and self.popup_window.get_visible():
print(f"[NotificationIndicator] Popup is open, refreshing...")
self.popup_window.refresh_popup()
def on_dnd_changed(self, _service, dnd_enabled):
"""Handle DND status changes."""
print(f"[NotificationIndicator] DND changed: {dnd_enabled}")
self.update_count() # This will update the icon based on DND status
# If popup is open, refresh it to reflect DND state
if self.popup_window and self.popup_window.get_visible():
print(f"[NotificationIndicator] DND changed, refreshing popup...")
self.popup_window.refresh_popup()
def update_count(self):
"""Update the notification icon display."""
count = self.notification_service.count
self.notification_count = count
dnd_enabled = self.notification_service.dont_disturb
if dnd_enabled:
# Show DND icon when DND is enabled
self.notification_icon.set_markup(icons.notifications_off)
self.set_tooltip_text(f"Do Not Disturb enabled ({count} notification{'s' if count != 1 else ''})")
elif count >= 1:
# Show notifications_dots icon when there are notifications
self.notification_icon.set_markup(icons.notifications_dots)
self.set_tooltip_text(f"{count} notification{'s' if count != 1 else ''}")
else:
# Show regular notifications icon when no notifications
self.notification_icon.set_markup(icons.notifications)
self.set_tooltip_text("No notifications")
def on_clicked(self, _button):
"""Handle click to show/hide notification popup."""
if self.popup_window and self.popup_window.get_visible():
self.popup_window.set_visible(False)
else:
self.show_notifications_popup()
def show_notifications_popup(self):
"""Show popup window with cached notifications."""
if self.popup_window:
self.popup_window.destroy()
# Get cached notifications (always get fresh data)
notifications = self.notification_service.get_deserialized()
print(f"[NotificationIndicator] Opening popup with {len(notifications)} notifications")
# Create popup window (handles both empty and filled states)
self.popup_window = NotificationPopupWindow(notifications, self.notification_service)
self.popup_window.show_all()
class NotificationPopupWindow(Window):
def __init__(self, notifications, notification_service):
self.notification_service = notification_service
# Create scrolled container for notifications
self.notifications_box = Box(
name="notifications-popup-box",
orientation="v",
spacing=8,
h_expand=True,
v_expand=False
)
if not notifications:
# Show "No notifications" message
no_notif_icon = Label(
name="no-notifications-icon",
markup=icons.notifications,
h_align="center"
)
no_notif_message = Label(
name="no-notifications-message",
label="No notifications",
h_align="center",
v_align="center"
)
no_notif_content = Box(
name="no-notifications-content",
orientation="v",
spacing=16,
h_align="center",
v_align="center",
h_expand=True,
v_expand=True,
children=[no_notif_icon, no_notif_message]
)
self.notifications_box.add(no_notif_content)
else:
# Add notifications to the box (limit to recent 10)
recent_notifications = notifications[-10:] if len(notifications) > 10 else notifications
recent_notifications.reverse() # Show newest first
for notification in recent_notifications:
# Create notification widget with dock callback passed during initialization
notif_widget = NotificationWidget(
notification,
show_progress=False,
dock_callback=self.remove_notification_from_cache
)
self.notifications_box.add(notif_widget)
# Create scrolled window with proper sizing
scrolled = ScrolledWindow(
name="notifications-scrolled",
child=self.notifications_box,
h_scrollbar_policy="never",
v_scrollbar_policy="automatic",
min_content_size=(400, 450),
max_content_size=(450, 500),
h_expand=True,
v_expand=True,
propagate_width=False,
propagate_height=False
)
# Create header with DND toggle and clear all button
header_children = [
Label(
name="notifications-title",
label="Notifications",
h_align="start",
h_expand=True
)
]
# Add DND toggle button with icon
dnd_icon = icons.notifications_off if self.notification_service.dont_disturb else icons.notifications
dnd_tooltip = "Do Not Disturb: ON" if self.notification_service.dont_disturb else "Do Not Disturb: OFF"
self.dnd_button = Button(
name="dnd-toggle-button",
child=Label(
name="dnd-toggle-icon",
markup=dnd_icon
),
on_clicked=self.toggle_dnd
)
self.dnd_button.set_tooltip_text(dnd_tooltip)
header_children.append(self.dnd_button)
if notifications: # Only add clear button if there are notifications
header_children.append(
Button(
name="clear-all-button",
label="Clear All",
on_clicked=self.clear_all_notifications
)
)
header_box = Box(
name="notifications-header",
orientation="h",
spacing=8,
children=header_children
)
main_box = Box(
name="notifications-popup-main",
orientation="v",
spacing=8,
h_expand=True,
v_expand=True,
children=[header_box, scrolled]
)
# Determine popup position based on dock position
dock_position = data.DOCK_POSITION
popup_anchor = self._get_popup_anchor(dock_position)
print(f"[NotificationPopup] Dock position: {dock_position}, Popup anchor: {popup_anchor}")
# Get margin based on dock position
margin = self._get_popup_margin(dock_position)
super().__init__(
name="notifications-popup-window",
anchor=popup_anchor,
margin=margin,
child=main_box,
layer="top",
exclusive=False,
keyboard_mode="on-demand",
visible=True,
all_visible=True
)
# Auto-hide when clicking outside or pressing escape
self.connect("button-press-event", self.on_button_press)
self.connect("key-press-event", self.on_key_press)
# Make sure the window can receive key events
self.set_can_focus(True)
self.grab_focus()
# Set tooltip with keyboard shortcuts
self.set_tooltip_text("Keyboard shortcuts:\n• Escape: Close popup\n• Ctrl+D: Toggle DND\n• Ctrl+A: Clear all notifications")
def _get_popup_anchor(self, dock_position):
"""Determine popup anchor position based on dock position."""
# Position popup on the opposite side of the dock for better UX
anchor_map = {
"Top": "top", # Dock at top -> popup at top right
"Bottom": "bottom", # Dock at bottom -> popup at bottom right
"Left": "left", # Dock at left -> popup at top left
"Right": "right" # Dock at right -> popup at top right
}
return anchor_map.get(dock_position, "bottom") # Default fallback
def _get_popup_margin(self, dock_position):
"""Get appropriate margin based on dock position."""
# Add margin to avoid overlapping with dock
margin_map = {
"Top": "60px 10px 10px 10px", # Top dock -> margin from top
"Bottom": "10px 10px 60px 10px", # Bottom dock -> margin from bottom
"Left": "10px 10px 10px 60px", # Left dock -> margin from left
"Right": "10px 60px 10px 10px" # Right dock -> margin from right
}
return margin_map.get(dock_position, "10px 10px 10px 10px") # Default fallback
def update_dnd_button_icon(self):
"""Update the DND button icon and tooltip to reflect current DND state."""
if hasattr(self, 'dnd_button'):
dnd_enabled = self.notification_service.dont_disturb
dnd_icon = icons.notifications_off if dnd_enabled else icons.notifications
dnd_tooltip = "Do Not Disturb: ON" if dnd_enabled else "Do Not Disturb: OFF"
# Update the button icon
button_label = self.dnd_button.get_child()
button_label.set_markup(dnd_icon)
# Update tooltip
self.dnd_button.set_tooltip_text(dnd_tooltip)
print(f"[NotificationPopup] Updated DND button icon: {dnd_icon}")
def remove_notification_from_cache(self, notification):
"""Remove a specific notification from cache and refresh popup."""
print(f"[NotificationPopup] Removing notification from cache: {notification.summary}")
# Find and remove the notification from the service cache
# We need to find it by matching summary and app_name since IDs might differ
all_notifications = self.notification_service.all_notifications
for i, cached_notif in enumerate(all_notifications):
if (cached_notif.get("summary") == notification.summary and
cached_notif.get("app_name") == notification.app_name):
print(f"[NotificationPopup] Found matching notification, removing...")
self.notification_service.all_notifications.pop(i)
self.notification_service._write_notifications(self.notification_service.all_notifications)
self.notification_service.emit("notification_count", len(self.notification_service.all_notifications))
break
# Refresh the popup immediately
self.refresh_popup()
def on_notification_closed(self, notification, reason):
"""Handle when a notification is closed - refresh the popup."""
# Small delay to allow the notification service to update
from gi.repository import GLib
GLib.timeout_add(100, self.refresh_popup)
def refresh_popup(self):
"""Refresh the popup content."""
# Get updated notifications
notifications = self.notification_service.get_deserialized()
# Clear current content
for child in self.notifications_box.get_children():
self.notifications_box.remove(child)
if not notifications:
# Show "No notifications" message
no_notif_icon = Label(
name="no-notifications-icon",
markup=icons.notifications,
h_align="center"
)
no_notif_message = Label(
name="no-notifications-message",
label="No notifications",
h_align="center",
v_align="center"
)
no_notif_content = Box(
name="no-notifications-content",
orientation="v",
spacing=16,
h_align="center",
v_align="center",
h_expand=True,
v_expand=True,
children=[no_notif_icon, no_notif_message]
)
self.notifications_box.add(no_notif_content)
else:
# Add updated notifications
recent_notifications = notifications[-10:] if len(notifications) > 10 else notifications
recent_notifications.reverse() # Show newest first
for notification in recent_notifications:
notif_widget = NotificationWidget(
notification,
show_progress=False,
dock_callback=self.remove_notification_from_cache
)
self.notifications_box.add(notif_widget)
self.notifications_box.show_all()
return False # Don't repeat the timeout
def toggle_dnd(self, button):
"""Toggle Do Not Disturb mode."""
current_dnd = self.notification_service.dont_disturb
new_dnd = not current_dnd
self.notification_service.dont_disturb = new_dnd
print(f"[NotificationPopup] DND toggled: {new_dnd}")
# Update the DND button icon using the centralized method
self.update_dnd_button_icon()
# Emit DND signal
self.notification_service.emit("dnd", new_dnd)
def clear_all_notifications(self, _button):
"""Clear all cached notifications."""
self.notification_service.clear_all_notifications()
self.set_visible(False)
def on_key_press(self, _widget, event):
"""Handle key press events."""
from gi.repository import Gdk
# Check if Escape key was pressed
if event.keyval == Gdk.KEY_Escape:
print(f"[NotificationPopup] Escape key pressed, closing popup")
self.set_visible(False)
return True # Event handled
# Check for Ctrl+D to toggle DND
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_d:
print(f"[NotificationPopup] Ctrl+D pressed, toggling DND")
# Toggle DND state directly
current_dnd = self.notification_service.dont_disturb
new_dnd = not current_dnd
self.notification_service.dont_disturb = new_dnd
print(f"[NotificationPopup] DND toggled via keyboard: {new_dnd}")
# Update the DND button icon immediately
self.update_dnd_button_icon()
# Emit DND signal
self.notification_service.emit("dnd", new_dnd)
return True # Event handled
# Check for Ctrl+A to clear all notifications
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_a:
print(f"[NotificationPopup] Ctrl+A pressed, clearing all notifications")
if self.notification_service.count > 0:
self.clear_all_notifications(None) # Pass None as button parameter
return True # Event handled
return False # Let other handlers process the event
def on_button_press(self, _widget, _event):
"""Hide popup when clicking outside."""
# This is a simple implementation - in a real scenario you'd want
# more sophisticated outside-click detection
return False
class Notifications(Box):
def __init__(self, **kwargs):
super().__init__(
name="notifications",
orientation="h" if not data.VERTICAL else "v",
spacing=4,
children=[NotificationIndicator()],
**kwargs
)
self.show_all()