458 lines
18 KiB
Python
458 lines
18 KiB
Python
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()
|