🔧 - Added Notifications component with dock and system tray indicators
This commit is contained in:
10 files changed
+937
-86
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user