From c0fd204b6a09402b22c32b75fc63ae6827fcc079 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 1 Mar 2025 21:09:53 +0545 Subject: [PATCH] update notification --- modules/notification_popup.py | 411 +++++++++++++++++----------------- 1 file changed, 200 insertions(+), 211 deletions(-) diff --git a/modules/notification_popup.py b/modules/notification_popup.py index 8835778..35982ff 100644 --- a/modules/notification_popup.py +++ b/modules/notification_popup.py @@ -1,70 +1,49 @@ -import time - -import gi - -gi.require_version("GdkPixbuf", "2.0") - +import os +from gi.repository import GdkPixbuf, GLib, Gdk from fabric.notifications import ( Notification, NotificationAction, NotificationCloseReason, Notifications, ) -from fabric.utils import invoke_repeater +from loguru import logger from fabric.widgets.box import Box from fabric.widgets.button import Button -from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.image import Image +from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label -from fabric.widgets.overlay import Overlay from fabric.widgets.revealer import Revealer from fabric.widgets.wayland import WaylandWindow as Window -from gi.repository import GdkPixbuf, GLib, GObject -from snippets import Animator, CustomImage - - -class AnimatedCircularProgressBar(CircularProgressBar): - def __init__(self, duration=0.6, bezier_curve=(0.34, 1.56, 0.64, 1.0), **kwargs): - super().__init__(**kwargs) - - self.animator = ( - Animator( - bezier_curve=bezier_curve, - duration=duration, - min_value=self.min_value, - max_value=self.value, - tick_widget=self, - notify_value=lambda p, *_: self.set_value(p.value), - ) - .build() - .play() - .unwrap() - ) - - def animate_value(self, value: float): - self.animator.pause() - self.animator.min_value = self.value - self.animator.max_value = value - self.animator.play() - return +from utils import CustomImage +import utils.icons as icons class ActionButton(Button): def __init__( - self, action: NotificationAction, action_number: int, total_actions: int + self, action: NotificationAction, index: int, total: int, notification_box ): - self.action = action super().__init__( - label=action.label, + 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) ) - if action_number == 0: - self.add_style_class("start-action") - elif action_number == total_actions - 1: - self.add_style_class("end-action") - else: - self.add_style_class("middle-action") def on_clicked(self, *_): self.action.invoke() @@ -72,180 +51,191 @@ class ActionButton(Button): class NotificationWidget(Box): - def __init__(self, notification: Notification, **kwargs): + def __init__(self, notification: Notification, timeout_ms=5000, **kwargs): super().__init__( - name="notification", - spacing=8, + name="notification-box", orientation="v", - **kwargs, - ) - - self._notification = notification - self._timer = None - self.anim_parts = 20 - self.anim_interval = self.get_timeout() / self.anim_parts - self.timeout_in_sec = self.get_timeout() / 1000 - self.time = GLib.DateTime.new_from_unix_local(time.time()).format("%m/%d") - self.connect("button-press-event", self.on_button_press) - - header_container = Box(spacing=8, orientation="h") - self.progress_timeout = AnimatedCircularProgressBar( - name="notification-circular-progress-bar", - size=30, - min_value=0, - max_value=1, - radius_color=True, - value=0, - ) - - header_container.children = ( - self.get_icon(notification.app_icon, notification.app_name, 25), - Label( - markup=str( - self._notification.summary - if self._notification.summary - else notification.app_name - ), - h_align="start", - style="font-weight: 900;", - ellipsization="end", - ), - ) - - header_container.pack_end( - Box( - v_align="start", - children=( - Overlay( - child=self.progress_timeout, - overlays=Button( - image=Image( - icon_name="window-close-symbolic", - icon_size=16, - ), - style_classes="close-button", - on_clicked=lambda *_: self._notification.close( - "dismissed-by-user" - ), - ), - ), - ), - ), - False, - False, - 0, - ) - - body_container = Box(spacing=4, orientation="h") - - if image_pixbuf := self._notification.image_pixbuf: - body_container.add( - Box( - v_expand=True, - v_align="center", - children=CustomImage( - pixbuf=image_pixbuf.scale_simple( - 64, 64, GdkPixbuf.InterpType.BILINEAR - ), - style_classes="image", - ), - ) - ) - - body_container.add( - Label( - markup=self._notification.body, - line_wrap="word-char", - v_align="start", - ellipsization="end", - max_chars_width=40, - h_align="start", - ) - ) - - actions_container = Box( - spacing=4, - orientation="h", - name="notification-action-buttons", - children=[ - ActionButton(action, i, len(self._notification.actions)) - for i, action in enumerate(self._notification.actions) - ], + 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, + ) ) - self.add(header_container) - self.add(body_container) - self.add(actions_container) - - # Destroy this widget once the notification is closed - self._notification.connect( - "closed", - lambda *_: ( - parent.remove(self) if (parent := self.get_parent()) else None, # type: ignore - self.destroy(), - ), - invoke_repeater( - 5 * 1000, - lambda: self._notification.close("expired"), - initial_call=False, - ), + 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()], ) - self.start_timer() - - def start_timer(self): - self._timer = GObject.timeout_add(200, self.update_progress) - - def update_progress(self): - self.progress_timeout.animate_value( - self.progress_timeout.value + 1 / self.anim_parts + 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), + ], + ), + ], ) - if self.progress_timeout.value >= self.timeout_in_sec: - self.progress_timeout.value = self.timeout_in_sec - self.stop_timer() - return True # Continue the timer - def stop_timer(self): - if self._timer: - GObject.source_remove(self._timer) - self._timer = None + def get_pixbuf(self, icon_path, width, height): + if icon_path.startswith("file://"): + icon_path = icon_path[7:] - def get_icon(self, app_icon, app_name, size) -> Image: - app_name = ( - app_name.lower().replace(" ", "-") - if isinstance(app_name, str) - else app_name + 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) + ], ) - if isinstance(app_icon, str) and app_icon.strip(): - if app_icon.startswith("file://") or app_icon.startswith("/"): - file_path = app_icon[7:] if app_icon.startswith("file://") else app_icon - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) - scaled_pixbuf = pixbuf.scale_simple( - size, size, GdkPixbuf.InterpType.BILINEAR - ) - return Image(name="notification-icon", pixbuf=scaled_pixbuf) - except Exception as e: - print(f"DEBUG: Failed to load image from {file_path}: {e}") - if app_icon and not app_icon.startswith(("file://", "/")): - icon_name = app_icon - elif app_name and app_name.strip() not in ["notify-send", ""]: - icon_name = app_name.strip() - else: - icon_name = "dialog-information-symbolic" - - return Image(name="notification-icon", icon_name=icon_name, icon_size=size) - - def on_button_press(self, _, event): - if event.button != 1: - (self._notification.close("dismissed-by-user"),) - - def get_timeout(self): - return ( - self._notification.timeout if self._notification.timeout != -1 else 5 * 1000 + 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): @@ -254,11 +244,10 @@ class NotificationRevealer(Revealer): self._notification = notification super().__init__( child=Box( - style="margin: 12px;", children=[self.notif_box], ), - transition_duration=500, - transition_type="crossfade", + transition_duration=250, + transition_type="slide-down", ) self.connect( @@ -289,7 +278,7 @@ class NotificationPopup(Window): self._server.connect("notification-added", self.on_new_notification) super().__init__( - anchor="bottom", + anchor="top right", child=self.notifications, layer="overlay", all_visible=True,