fix: resolve choppy animations during notification transitions
Co-authored-by: aider (openai/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
1 parent
305a105b22
commit
abe5dfe1c7
2 files changed
+112
-49
No files matched your search
@@ -262,6 +262,7 @@ class NotificationRevealer(SlideRevealer):
|
||||
self.on_transition_end = on_transition_end
|
||||
# Reference to ModusNoti window for queue clearing
|
||||
self.parent_window = parent_window
|
||||
self._is_closing = False
|
||||
|
||||
# Add swipe detection variables
|
||||
self._drag_start_x = 0
|
||||
@@ -308,6 +309,11 @@ class NotificationRevealer(SlideRevealer):
|
||||
_notification: Notification,
|
||||
reason: NotificationCloseReason,
|
||||
):
|
||||
if self._is_closing:
|
||||
return
|
||||
|
||||
self._is_closing = True
|
||||
|
||||
# Use left-to-right slide for auto-dismiss (expired), right-to-left for manual close
|
||||
if reason == "expired":
|
||||
# Left-to-right slide for auto-dismiss
|
||||
@@ -361,6 +367,12 @@ class NotificationRevealer(SlideRevealer):
|
||||
return False
|
||||
|
||||
|
||||
class NotificationState:
|
||||
IDLE = 0
|
||||
SHOWING = 1
|
||||
HIDING = 2
|
||||
|
||||
|
||||
class ModusNoti(Window):
|
||||
def __init__(self):
|
||||
self._server = notification_service
|
||||
@@ -373,10 +385,11 @@ class ModusNoti(Window):
|
||||
spacing=5,
|
||||
)
|
||||
|
||||
# Queue system for macOS-like behavior
|
||||
# Enhanced queue system for smooth transitions
|
||||
self.notification_queue = []
|
||||
self.current_notification = None
|
||||
self.is_showing_notification = False
|
||||
self.notification_state = NotificationState.IDLE
|
||||
self._transition_timer_id = None
|
||||
|
||||
# Initialize ignored apps list from config
|
||||
self.ignored_apps = data.NOTIFICATION_IGNORED_APPS
|
||||
@@ -411,6 +424,7 @@ class ModusNoti(Window):
|
||||
notification.close("dismissed-by-user")
|
||||
return
|
||||
|
||||
# Clear any pending notifications in queue (except the current one being shown)
|
||||
for pending_notification in list(self.notification_queue):
|
||||
try:
|
||||
pending_notification.close("dismissed-by-user")
|
||||
@@ -418,46 +432,54 @@ class ModusNoti(Window):
|
||||
pass
|
||||
self.notification_queue.clear()
|
||||
|
||||
if self.current_notification and self.is_showing_notification:
|
||||
current_allocation = None
|
||||
try:
|
||||
if self.current_notification.get_parent():
|
||||
current_allocation = self.current_notification.get_allocation()
|
||||
except:
|
||||
pass
|
||||
|
||||
if hasattr(self.current_notification, "stop_animation"):
|
||||
self.current_notification.stop_animation()
|
||||
|
||||
try:
|
||||
if self.current_notification in self.notifications.children:
|
||||
self.notifications.remove(self.current_notification)
|
||||
if hasattr(self.current_notification, "notification"):
|
||||
self.current_notification.notification.close("dismissed-by-user")
|
||||
except:
|
||||
pass
|
||||
|
||||
self.current_notification = None
|
||||
self.is_showing_notification = False
|
||||
|
||||
# Add new notification to queue
|
||||
self.notification_queue.append(notification)
|
||||
self.show_next_notification()
|
||||
|
||||
# Process the queue
|
||||
self._process_notification_queue()
|
||||
|
||||
def show_next_notification(self):
|
||||
if not self.notification_queue or self.is_showing_notification:
|
||||
def _process_notification_queue(self):
|
||||
# If we're currently showing a notification and there's a new one in queue
|
||||
if (self.notification_state == NotificationState.SHOWING and
|
||||
self.current_notification and
|
||||
self.notification_queue):
|
||||
|
||||
# Start hiding the current notification to make room for the new one
|
||||
self._start_hiding_current_notification()
|
||||
|
||||
elif self.notification_state == NotificationState.IDLE and self.notification_queue:
|
||||
# If we're idle and have notifications in queue, show the next one
|
||||
self._show_next_notification()
|
||||
|
||||
def _start_hiding_current_notification(self):
|
||||
if (self.current_notification and
|
||||
self.notification_state == NotificationState.SHOWING and
|
||||
not self.current_notification._is_closing):
|
||||
|
||||
self.notification_state = NotificationState.HIDING
|
||||
|
||||
# Force close the current notification to trigger hiding animation
|
||||
try:
|
||||
self.current_notification.notification.close("dismissed-by-user")
|
||||
except:
|
||||
pass
|
||||
|
||||
def _show_next_notification(self):
|
||||
if not self.notification_queue or self.notification_state != NotificationState.IDLE:
|
||||
return
|
||||
|
||||
notification = self.notification_queue.pop(0)
|
||||
self.is_showing_notification = True
|
||||
self.notification_state = NotificationState.SHOWING
|
||||
|
||||
new_box = NotificationRevealer(
|
||||
notification,
|
||||
on_transition_end=lambda: self.on_notification_finished(new_box),
|
||||
on_transition_end=lambda: self._on_notification_finished(new_box),
|
||||
parent_window=self,
|
||||
)
|
||||
|
||||
self.current_notification = new_box
|
||||
|
||||
# Clear any existing children
|
||||
for child in list(self.notifications.children):
|
||||
try:
|
||||
self.notifications.remove(child)
|
||||
@@ -465,38 +487,51 @@ class ModusNoti(Window):
|
||||
pass
|
||||
|
||||
self.notifications.children = [new_box]
|
||||
|
||||
new_box.show_all()
|
||||
self.notifications.queue_resize()
|
||||
|
||||
def start_animation():
|
||||
if new_box.get_parent():
|
||||
if new_box.get_realized():
|
||||
new_box.reveal()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
if new_box.get_parent() and new_box.get_realized():
|
||||
new_box.reveal()
|
||||
return False
|
||||
return True
|
||||
|
||||
GLib.idle_add(start_animation)
|
||||
|
||||
def on_notification_finished(self, notification_box):
|
||||
def _on_notification_finished(self, notification_box):
|
||||
if notification_box != self.current_notification:
|
||||
return
|
||||
|
||||
# Cancel any pending transition timer
|
||||
if self._transition_timer_id:
|
||||
GLib.source_remove(self._transition_timer_id)
|
||||
self._transition_timer_id = None
|
||||
|
||||
# Safely remove notification box
|
||||
try:
|
||||
if notification_box in self.notifications.children:
|
||||
self.notifications.remove(notification_box)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Reset state
|
||||
self.current_notification = None
|
||||
self.is_showing_notification = False
|
||||
self.notification_state = NotificationState.IDLE
|
||||
|
||||
# Process next notification with a small delay for smooth transitions
|
||||
if self.notification_queue:
|
||||
# Reduced delay for more responsive queuing
|
||||
GLib.timeout_add(80, lambda: self.show_next_notification() or False)
|
||||
self._transition_timer_id = GLib.timeout_add(
|
||||
50, # Very short delay for seamless transitions
|
||||
lambda: self._show_next_notification() or False
|
||||
)
|
||||
|
||||
def show_next_notification(self):
|
||||
# Legacy method for compatibility - redirect to new implementation
|
||||
self._show_next_notification()
|
||||
|
||||
def on_notification_finished(self, notification_box):
|
||||
# Legacy method for compatibility - redirect to new implementation
|
||||
self._on_notification_finished(notification_box)
|
||||
|
||||
def clear_notification_queue(self):
|
||||
queue_length = len(self.notification_queue)
|
||||
@@ -508,5 +543,10 @@ class ModusNoti(Window):
|
||||
pass # Ignore errors if notification is already closed
|
||||
self.notification_queue.clear()
|
||||
|
||||
# Cancel any pending transition timer
|
||||
if self._transition_timer_id:
|
||||
GLib.source_remove(self._transition_timer_id)
|
||||
self._transition_timer_id = None
|
||||
|
||||
def get_queue_length(self):
|
||||
return len(self.notification_queue)
|
||||
@@ -39,6 +39,8 @@ class AnimationManager:
|
||||
self._containers_to_redraw.clear()
|
||||
|
||||
widgets_to_remove = []
|
||||
|
||||
# Process all animations in a single frame
|
||||
for widget in list(self._animating_widgets):
|
||||
if not widget._calculate_position():
|
||||
widgets_to_remove.append(widget)
|
||||
@@ -47,11 +49,11 @@ class AnimationManager:
|
||||
if container:
|
||||
self._containers_to_redraw.add(container)
|
||||
|
||||
# Apply all position changes at once
|
||||
# Apply all position changes at once to prevent conflicts
|
||||
for widget in self._animating_widgets:
|
||||
widget._apply_position()
|
||||
|
||||
# Single redraw call per container
|
||||
# Batch redraw calls to minimize performance impact
|
||||
for container in self._containers_to_redraw:
|
||||
container.queue_draw()
|
||||
|
||||
@@ -61,6 +63,10 @@ class AnimationManager:
|
||||
|
||||
return len(self._animating_widgets) > 0 # Continue if widgets remain
|
||||
|
||||
def get_active_widget_count(self):
|
||||
"""Return the number of currently animating widgets"""
|
||||
return len(self._animating_widgets)
|
||||
|
||||
def _get_optimal_interval(self):
|
||||
"""Keep consistent 120 FPS for macOS-like smoothness"""
|
||||
return 8 # 120 FPS
|
||||
@@ -99,6 +105,16 @@ class MacOSEasing:
|
||||
"""Custom cubic bezier similar to macOS default (0.25, 0.1, 0.25, 1.0)"""
|
||||
# Approximation of cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
||||
return t * t * t * (t * (6 * t - 15) + 10)
|
||||
|
||||
@staticmethod
|
||||
def ease_in_cubic(t):
|
||||
"""Cubic ease in for smooth acceleration"""
|
||||
return t * t * t
|
||||
|
||||
@staticmethod
|
||||
def ease_out_quint(t):
|
||||
"""Quintic ease out for very smooth deceleration"""
|
||||
return 1 - math.pow(1 - t, 5)
|
||||
|
||||
|
||||
class SlideRevealer(Gtk.Overlay):
|
||||
@@ -115,6 +131,7 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self._show_animation = False
|
||||
self._pending_position = None
|
||||
self._current_position = (0.0, 0.0) # Use float for sub-pixel positioning
|
||||
self._animation_id = None # Track individual animation instances
|
||||
|
||||
self._fixed = Gtk.Fixed()
|
||||
self._fixed.set_has_window(False)
|
||||
@@ -146,18 +163,19 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self.hide()
|
||||
|
||||
def reveal(self):
|
||||
if self._revealed or self._animating:
|
||||
if self._revealed and not self._animating:
|
||||
return
|
||||
self._revealed = True
|
||||
self._start_animation(show=True)
|
||||
|
||||
def hide(self):
|
||||
if not self._revealed or self._animating:
|
||||
if not self._revealed and not self._animating:
|
||||
return
|
||||
self._revealed = False
|
||||
self._start_animation(show=False)
|
||||
|
||||
def _start_animation(self, show: bool):
|
||||
# Stop any existing animation for this widget
|
||||
if self._animating:
|
||||
AnimationManager.get_instance().remove_widget(self)
|
||||
|
||||
@@ -166,9 +184,11 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self._animating = False
|
||||
return
|
||||
|
||||
# Use high-precision monotonic time for smooth animations
|
||||
self._start_time = GLib.get_monotonic_time()
|
||||
self._animating = True
|
||||
self._show_animation = show
|
||||
self._animation_id = id(self) # Unique ID for this animation instance
|
||||
|
||||
if show:
|
||||
self.child.show()
|
||||
@@ -185,18 +205,20 @@ class SlideRevealer(Gtk.Overlay):
|
||||
elapsed = (GLib.get_monotonic_time() - self._start_time) / 1000
|
||||
t = min(elapsed / self.duration, 1.0)
|
||||
|
||||
# Use different easing functions for better smoothness
|
||||
if self._show_animation:
|
||||
# Use macOS-style ease out for revealing (fast start, smooth end)
|
||||
eased = MacOSEasing.ease_out_cubic_bezier(t)
|
||||
# Use quintic ease out for very smooth revealing
|
||||
eased = MacOSEasing.ease_out_quint(t)
|
||||
else:
|
||||
# Use ease in-out for hiding (smooth both ends)
|
||||
eased = MacOSEasing.ease_in_out_quart(t)
|
||||
# Use cubic ease in for smooth hiding
|
||||
eased = MacOSEasing.ease_in_cubic(t)
|
||||
|
||||
self._pending_position = self._get_position_at_progress_cached(eased)
|
||||
|
||||
if t >= 1.0:
|
||||
self._animating = False
|
||||
self._cached_dimensions = None
|
||||
self._animation_id = None
|
||||
if not self._show_animation:
|
||||
GLib.idle_add(lambda: self.child.hide())
|
||||
return False
|
||||
@@ -275,6 +297,7 @@ class SlideRevealer(Gtk.Overlay):
|
||||
AnimationManager.get_instance().remove_widget(self)
|
||||
self._animating = False
|
||||
self._cached_dimensions = None
|
||||
self._animation_id = None
|
||||
|
||||
def destroy(self):
|
||||
self.stop_animation()
|
||||
|
||||
Reference in new issue
Block a user