307 lines
10 KiB
Python
307 lines
10 KiB
Python
import os
|
|
from gi.repository import GdkPixbuf, GLib, Gdk
|
|
from fabric.notifications import (
|
|
Notification,
|
|
NotificationAction,
|
|
NotificationCloseReason,
|
|
)
|
|
from loguru import logger
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.button import Button
|
|
from fabric.widgets.image import Image
|
|
from fabric.widgets.centerbox import CenterBox
|
|
from fabric.widgets.label import Label
|
|
from fabric.widgets.revealer import Revealer
|
|
from utils.wayland import WaylandWindow as Window
|
|
from utils import CustomImage
|
|
import utils.icons as icons
|
|
from services import notification_service
|
|
|
|
|
|
class ActionButton(Button):
|
|
def __init__(
|
|
self, action: NotificationAction, index: int, total: int, notification_box
|
|
):
|
|
super().__init__(
|
|
name="action-button",
|
|
h_expand=True,
|
|
on_clicked=self.on_clicked,
|
|
child=Label(name="button-label", label=action.label),
|
|
)
|
|
self.action = action
|
|
self.notification_box = notification_box
|
|
style_class = (
|
|
"start-action"
|
|
if index == 0
|
|
else "end-action"
|
|
if index == total - 1
|
|
else "middle-action"
|
|
)
|
|
self.add_style_class(style_class)
|
|
self.connect(
|
|
"enter-notify-event", lambda *_: notification_box.hover_button(self)
|
|
)
|
|
self.connect(
|
|
"leave-notify-event", lambda *_: notification_box.unhover_button(self)
|
|
)
|
|
|
|
def on_clicked(self, *_):
|
|
self.action.invoke()
|
|
self.action.parent.close("dismissed-by-user")
|
|
|
|
|
|
class NotificationWidget(Box):
|
|
def __init__(self, notification: Notification, timeout_ms=5000, **kwargs):
|
|
super().__init__(
|
|
name="notification-box",
|
|
orientation="v",
|
|
h_align="fill",
|
|
h_expand=True,
|
|
children=[
|
|
self.create_content(notification),
|
|
self.create_action_buttons(notification),
|
|
],
|
|
)
|
|
self.notification = notification
|
|
self.timeout_ms = timeout_ms
|
|
self._timeout_id = None
|
|
self.start_timeout()
|
|
|
|
def create_header(self, notification):
|
|
app_icon = (
|
|
Image(
|
|
name="notification-icon",
|
|
image_file=notification.app_icon[7:],
|
|
size=24,
|
|
)
|
|
if "file://" in notification.app_icon
|
|
else Image(
|
|
name="notification-icon",
|
|
icon_name="dialog-information-symbolic" or notification.app_icon,
|
|
icon_size=24,
|
|
)
|
|
)
|
|
|
|
return CenterBox(
|
|
name="notification-title",
|
|
start_children=[
|
|
Box(
|
|
spacing=4,
|
|
children=[
|
|
app_icon,
|
|
Label(
|
|
notification.app_name,
|
|
name="notification-app-name",
|
|
h_align="start",
|
|
),
|
|
],
|
|
)
|
|
],
|
|
end_children=[self.create_close_button()],
|
|
)
|
|
|
|
def create_content(self, notification):
|
|
return Box(
|
|
name="notification-content",
|
|
spacing=8,
|
|
children=[
|
|
Box(
|
|
name="notification-image",
|
|
children=CustomImage(
|
|
pixbuf=notification.image_pixbuf.scale_simple(
|
|
48, 48, GdkPixbuf.InterpType.BILINEAR
|
|
)
|
|
if notification.image_pixbuf
|
|
else self.get_pixbuf(notification.app_icon, 48, 48)
|
|
),
|
|
),
|
|
Box(
|
|
name="notification-text",
|
|
orientation="v",
|
|
v_align="center",
|
|
children=[
|
|
Box(
|
|
name="notification-summary-box",
|
|
orientation="h",
|
|
children=[
|
|
Label(
|
|
name="notification-summary",
|
|
markup=notification.summary.replace("\n", " "),
|
|
h_align="start",
|
|
ellipsization="end",
|
|
),
|
|
Label(
|
|
name="notification-app-name",
|
|
markup=" | " + notification.app_name,
|
|
h_align="start",
|
|
ellipsization="end",
|
|
),
|
|
],
|
|
),
|
|
Label(
|
|
markup=notification.body.replace("\n", " "),
|
|
h_align="start",
|
|
ellipsization="end",
|
|
)
|
|
if notification.body
|
|
else Box(),
|
|
],
|
|
),
|
|
Box(h_expand=True),
|
|
Box(
|
|
orientation="v",
|
|
children=[
|
|
self.create_close_button(),
|
|
Box(v_expand=True),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
|
|
def get_pixbuf(self, icon_path, width, height):
|
|
if icon_path.startswith("file://"):
|
|
icon_path = icon_path[7:]
|
|
|
|
if not os.path.exists(icon_path):
|
|
logger.warning(f"Icon path does not exist: {icon_path}")
|
|
return None
|
|
|
|
try:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
|
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load or scale icon: {e}")
|
|
return None
|
|
|
|
def create_action_buttons(self, notification):
|
|
return Box(
|
|
name="notification-action-buttons",
|
|
spacing=4,
|
|
h_expand=True,
|
|
children=[
|
|
ActionButton(action, i, len(notification.actions), self)
|
|
for i, action in enumerate(notification.actions)
|
|
],
|
|
)
|
|
|
|
def create_close_button(self):
|
|
close_button = Button(
|
|
name="notif-close-button",
|
|
child=Label(name="notif-close-label", markup=icons.cancel),
|
|
on_clicked=lambda *_: self.notification.close("dismissed-by-user"),
|
|
)
|
|
close_button.connect(
|
|
"enter-notify-event", lambda *_: self.hover_button(close_button)
|
|
)
|
|
close_button.connect(
|
|
"leave-notify-event", lambda *_: self.unhover_button(close_button)
|
|
)
|
|
return close_button
|
|
|
|
def start_timeout(self):
|
|
self.stop_timeout()
|
|
self._timeout_id = GLib.timeout_add(self.timeout_ms, self.close_notification)
|
|
|
|
def stop_timeout(self):
|
|
if self._timeout_id is not None:
|
|
GLib.source_remove(self._timeout_id)
|
|
self._timeout_id = None
|
|
|
|
def close_notification(self):
|
|
self.notification.close("expired")
|
|
self.stop_timeout()
|
|
return False
|
|
|
|
def pause_timeout(self):
|
|
self.stop_timeout()
|
|
|
|
def resume_timeout(self):
|
|
self.start_timeout()
|
|
|
|
def destroy(self):
|
|
self.stop_timeout()
|
|
super().destroy()
|
|
|
|
# @staticmethod
|
|
def set_pointer_cursor(self, widget, cursor_name):
|
|
window = widget.get_window()
|
|
if window:
|
|
cursor = Gdk.Cursor.new_from_name(widget.get_display(), cursor_name)
|
|
window.set_cursor(cursor)
|
|
|
|
def hover_button(self, button):
|
|
self.pause_timeout()
|
|
self.set_pointer_cursor(button, "hand2")
|
|
|
|
def unhover_button(self, button):
|
|
self.resume_timeout()
|
|
self.set_pointer_cursor(button, "arrow")
|
|
|
|
|
|
class NotificationRevealer(Revealer):
|
|
def __init__(self, notification: Notification, **kwargs):
|
|
self.notif_box = NotificationWidget(notification)
|
|
self._notification = notification
|
|
super().__init__(
|
|
child=Box(
|
|
children=[self.notif_box],
|
|
),
|
|
transition_duration=250,
|
|
transition_type="slide-down",
|
|
)
|
|
|
|
self.connect(
|
|
"notify::child-revealed",
|
|
lambda *_: self.destroy() if not self.get_child_revealed() else None,
|
|
)
|
|
|
|
self._notification.connect("closed", self.on_resolved)
|
|
|
|
def on_resolved(
|
|
self,
|
|
notification: Notification,
|
|
reason: NotificationCloseReason,
|
|
):
|
|
self.set_reveal_child(False)
|
|
|
|
|
|
class NotificationPopup(Window):
|
|
def __init__(self):
|
|
self._server = notification_service
|
|
self.cache_notification_service = notification_service
|
|
self.notifications = Box(
|
|
v_expand=True,
|
|
h_expand=True,
|
|
style="margin: 1px 0px 1px 1px;",
|
|
orientation="v",
|
|
spacing=5,
|
|
)
|
|
self._server.connect("notification-added", self.on_new_notification)
|
|
|
|
super().__init__(
|
|
anchor="top right",
|
|
child=self.notifications,
|
|
layer="overlay",
|
|
all_visible=True,
|
|
visible=True,
|
|
exclusive=False,
|
|
)
|
|
|
|
def on_new_notification(self, fabric_notif, id):
|
|
notification: Notification = fabric_notif.get_notification_from_id(id)
|
|
# Always cache the notification regardless of DND status
|
|
self.cache_notification_service.cache_notification(notification)
|
|
|
|
# Only show popup if DND is not enabled
|
|
if not self.cache_notification_service.dont_disturb:
|
|
new_box = NotificationRevealer(fabric_notif.get_notification_from_id(id))
|
|
self.notifications.add(new_box)
|
|
new_box.set_reveal_child(True)
|
|
logger.info(f"[Notification] New notification from {notification.app_name}")
|
|
else:
|
|
logger.info(
|
|
f"[Notification] DND enabled, notification from {
|
|
notification.app_name
|
|
} not shown"
|
|
)
|