fix(notification_center): reconcile groups in place
This commit is contained in:
1 file changed
+50
-135
@@ -1,6 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from fabric.utils import Gdk, GdkPixbuf, GLib, Gtk, logger
|
from fabric.utils import Gdk, GLib, Gtk, logger
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.button import Button
|
from fabric.widgets.button import Button
|
||||||
from fabric.widgets.centerbox import CenterBox
|
from fabric.widgets.centerbox import CenterBox
|
||||||
@@ -20,18 +20,11 @@ from utils.functions import clear_children, escape_markup_text
|
|||||||
from window.notification.notification import (
|
from window.notification.notification import (
|
||||||
NotificationWidget,
|
NotificationWidget,
|
||||||
_get_relative_time,
|
_get_relative_time,
|
||||||
cache_notification_icon,
|
|
||||||
cleanup_all_notification_caches,
|
cleanup_all_notification_caches,
|
||||||
cleanup_notification_specific_caches,
|
cleanup_notification_specific_caches,
|
||||||
get_cached_notification_image,
|
get_notification_pixbuf,
|
||||||
preload_notification_assets,
|
preload_notification_assets,
|
||||||
)
|
)
|
||||||
from window.notification.unified_cache import (
|
|
||||||
get_fallback_icon as get_fallback_notification_icon,
|
|
||||||
)
|
|
||||||
from window.notification.unified_cache import (
|
|
||||||
get_from_cache,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExpandableNotificationGroup(CloseButtonRevealerMixin, Box):
|
class ExpandableNotificationGroup(CloseButtonRevealerMixin, Box):
|
||||||
@@ -179,68 +172,18 @@ class ExpandableNotificationGroup(CloseButtonRevealerMixin, Box):
|
|||||||
self.collapsed_eventbox.add(stack_container)
|
self.collapsed_eventbox.add(stack_container)
|
||||||
|
|
||||||
self.add(self.collapsed_eventbox)
|
self.add(self.collapsed_eventbox)
|
||||||
|
# Keep the collapsed view as the first child even after incremental
|
||||||
|
# refreshes recreate it.
|
||||||
|
self.reorder_child(self.collapsed_eventbox, 0)
|
||||||
|
|
||||||
def _get_notification_pixbuf_for_group(self, cached_notification):
|
def _get_notification_pixbuf_for_group(self, cached_notification):
|
||||||
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
||||||
notification = cached_notification._notification
|
return get_notification_pixbuf(
|
||||||
pixbuf = None
|
cached_notification._notification,
|
||||||
|
cache_metadata=getattr(cached_notification, "cache_metadata", {}) or {},
|
||||||
# First try to get cached notification image using stored cache key
|
target_size=(64, 64),
|
||||||
if (
|
include_live_image=False,
|
||||||
hasattr(cached_notification, "cache_metadata")
|
)
|
||||||
and cached_notification.cache_metadata
|
|
||||||
):
|
|
||||||
notification_image_cache_key = cached_notification.cache_metadata.get(
|
|
||||||
"notification_image_cache_key"
|
|
||||||
)
|
|
||||||
if notification_image_cache_key:
|
|
||||||
try:
|
|
||||||
cached_image = get_cached_notification_image(
|
|
||||||
notification_image_cache_key
|
|
||||||
)
|
|
||||||
if cached_image:
|
|
||||||
pixbuf = cached_image
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to load cached notification image: {e}")
|
|
||||||
|
|
||||||
# Fallback to app icon using cached key
|
|
||||||
if not pixbuf and (
|
|
||||||
hasattr(cached_notification, "cache_metadata")
|
|
||||||
and cached_notification.cache_metadata
|
|
||||||
):
|
|
||||||
app_icon_cache_key = cached_notification.cache_metadata.get(
|
|
||||||
"app_icon_cache_key"
|
|
||||||
)
|
|
||||||
if app_icon_cache_key:
|
|
||||||
try:
|
|
||||||
cached_app_icon = get_from_cache(app_icon_cache_key, (64, 64))
|
|
||||||
if cached_app_icon:
|
|
||||||
pixbuf = cached_app_icon
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to load cached app icon: {e}")
|
|
||||||
|
|
||||||
# Final fallback - try to cache app icon directly if available
|
|
||||||
if not pixbuf:
|
|
||||||
try:
|
|
||||||
app_icon_source = getattr(notification, "app_icon", None)
|
|
||||||
if app_icon_source:
|
|
||||||
cached_app_icon = cache_notification_icon(app_icon_source, (64, 64))
|
|
||||||
if cached_app_icon:
|
|
||||||
pixbuf = cached_app_icon
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to get directly cached app icon: {e}")
|
|
||||||
|
|
||||||
if not pixbuf:
|
|
||||||
pixbuf = get_fallback_notification_icon((64, 64))
|
|
||||||
|
|
||||||
# Always scale to 64x64 so screenshot thumbnails match app icon size
|
|
||||||
try:
|
|
||||||
return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"[notification_center] return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.B... failed: {e}"
|
|
||||||
)
|
|
||||||
return pixbuf
|
|
||||||
|
|
||||||
def create_expanded_state(self):
|
def create_expanded_state(self):
|
||||||
# Create main expanded container
|
# Create main expanded container
|
||||||
@@ -381,18 +324,36 @@ class ExpandableNotificationGroup(CloseButtonRevealerMixin, Box):
|
|||||||
self.expanded_container.set_visible(False)
|
self.expanded_container.set_visible(False)
|
||||||
return False # Don't repeat timeout
|
return False # Don't repeat timeout
|
||||||
|
|
||||||
def _refresh_expanded_state(self):
|
def reconcile_expanded_state(self):
|
||||||
"""Update the expanded notification list in-place without collapsing"""
|
"""Add/remove expanded list children in place, preserving newest-first order.
|
||||||
for child in list(self.notifications_list.get_children()):
|
|
||||||
self.notifications_list.remove(child)
|
|
||||||
child.destroy()
|
|
||||||
|
|
||||||
for notification in self.notifications:
|
Avoids tearing down every ``NotificationCenterWidget`` (and its action
|
||||||
notification_widget = NotificationCenterWidget(notification=notification)
|
menu) when a single notification is added or removed.
|
||||||
self.notifications_list.add(notification_widget)
|
"""
|
||||||
|
wanted_ids = {n.cache_id for n in self.notifications}
|
||||||
|
for child in list(self.notifications_list.get_children()):
|
||||||
|
if getattr(child, "notification_id", None) not in wanted_ids:
|
||||||
|
self.notifications_list.remove(child)
|
||||||
|
child.destroy()
|
||||||
|
|
||||||
|
by_id = {
|
||||||
|
getattr(c, "notification_id", None): c
|
||||||
|
for c in self.notifications_list.get_children()
|
||||||
|
}
|
||||||
|
for i, notification in enumerate(self.notifications):
|
||||||
|
if notification.cache_id in by_id:
|
||||||
|
continue
|
||||||
|
widget = NotificationCenterWidget(notification=notification)
|
||||||
|
self.notifications_list.add(widget)
|
||||||
|
self.notifications_list.reorder_child(widget, i)
|
||||||
|
by_id[notification.cache_id] = widget
|
||||||
|
|
||||||
self.notifications_list.show_all()
|
self.notifications_list.show_all()
|
||||||
|
|
||||||
|
def _refresh_expanded_state(self):
|
||||||
|
"""Update the expanded notification list in-place without collapsing"""
|
||||||
|
self.reconcile_expanded_state()
|
||||||
|
|
||||||
def close_all(self, *args):
|
def close_all(self, *args):
|
||||||
"""Close all notifications in this group with proper cache cleanup"""
|
"""Close all notifications in this group with proper cache cleanup"""
|
||||||
# Close all notifications in this group
|
# Close all notifications in this group
|
||||||
@@ -476,56 +437,12 @@ class NotificationCenterWidget(CloseButtonRevealerMixin, NotificationWidget):
|
|||||||
|
|
||||||
def _get_notification_pixbuf(self, notification):
|
def _get_notification_pixbuf(self, notification):
|
||||||
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
||||||
pixbuf = None
|
return get_notification_pixbuf(
|
||||||
|
notification,
|
||||||
# First try to get cached notification image using stored cache key
|
cache_metadata=self.cache_metadata,
|
||||||
if self.cache_metadata:
|
target_size=(64, 64),
|
||||||
notification_image_cache_key = self.cache_metadata.get(
|
include_live_image=False,
|
||||||
"notification_image_cache_key"
|
)
|
||||||
)
|
|
||||||
if notification_image_cache_key:
|
|
||||||
try:
|
|
||||||
cached_image = get_cached_notification_image(
|
|
||||||
notification_image_cache_key
|
|
||||||
)
|
|
||||||
if cached_image:
|
|
||||||
pixbuf = cached_image
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to load cached notification image: {e}")
|
|
||||||
|
|
||||||
# Fallback to app icon using cached key
|
|
||||||
if not pixbuf and self.cache_metadata:
|
|
||||||
app_icon_cache_key = self.cache_metadata.get("app_icon_cache_key")
|
|
||||||
if app_icon_cache_key:
|
|
||||||
try:
|
|
||||||
cached_app_icon = get_from_cache(app_icon_cache_key, (64, 64))
|
|
||||||
if cached_app_icon:
|
|
||||||
pixbuf = cached_app_icon
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to load cached app icon: {e}")
|
|
||||||
|
|
||||||
# Final fallback - try to cache app icon directly if available
|
|
||||||
if not pixbuf:
|
|
||||||
try:
|
|
||||||
app_icon_source = getattr(notification, "app_icon", None)
|
|
||||||
if app_icon_source:
|
|
||||||
cached_app_icon = cache_notification_icon(app_icon_source, (64, 64))
|
|
||||||
if cached_app_icon:
|
|
||||||
pixbuf = cached_app_icon
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to get directly cached app icon: {e}")
|
|
||||||
|
|
||||||
if not pixbuf:
|
|
||||||
pixbuf = get_fallback_notification_icon((64, 64))
|
|
||||||
|
|
||||||
# Always scale to 64x64 so screenshot thumbnails match app icon size
|
|
||||||
try:
|
|
||||||
return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"[notification_center] return pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.B... failed: {e}"
|
|
||||||
)
|
|
||||||
return pixbuf
|
|
||||||
|
|
||||||
def create_content(self, notification):
|
def create_content(self, notification):
|
||||||
self.close_button = Button(
|
self.close_button = Button(
|
||||||
@@ -912,20 +829,18 @@ class NotificationCenter(AppletWindow):
|
|||||||
"""Refresh a group widget's content, preserving expanded state"""
|
"""Refresh a group widget's content, preserving expanded state"""
|
||||||
try:
|
try:
|
||||||
if group_widget.is_expanded:
|
if group_widget.is_expanded:
|
||||||
children = list(group_widget.notifications_list.get_children())
|
group_widget.reconcile_expanded_state()
|
||||||
current_ids = {n.cache_id for n in group_widget.notifications}
|
|
||||||
for child in children:
|
|
||||||
if getattr(child, "notification_id", None) not in current_ids:
|
|
||||||
group_widget.notifications_list.remove(child)
|
|
||||||
child.destroy()
|
|
||||||
# Only show expanded container, not the whole group
|
# Only show expanded container, not the whole group
|
||||||
group_widget.expanded_container.show_all()
|
group_widget.expanded_container.show_all()
|
||||||
return
|
return
|
||||||
|
|
||||||
clear_children(group_widget)
|
# Collapsed: rebuild only the collapsed (latest notification) view,
|
||||||
|
# reconcile the expanded list incrementally instead of tearing down
|
||||||
|
# every child widget on each added notification.
|
||||||
|
if hasattr(group_widget, "collapsed_eventbox"):
|
||||||
|
group_widget.collapsed_eventbox.destroy()
|
||||||
group_widget.create_collapsed_state()
|
group_widget.create_collapsed_state()
|
||||||
group_widget.create_expanded_state()
|
group_widget.reconcile_expanded_state()
|
||||||
|
|
||||||
group_widget.show_all()
|
group_widget.show_all()
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user