diff --git a/modules/notification/notification.py b/modules/notification/notification.py index 457af8f..654c353 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -296,12 +296,22 @@ class NotificationRevealer(SlideRevealer): self.parent_window = parent_window self._is_closing = False - # Add swipe detection variables + # Enhanced swipe detection variables for Android-style animation self._drag_start_x = 0 self._drag_start_y = 0 self._is_dragging = False - self._swipe_threshold = 80 # Reduced threshold for more responsive swipes + self._swipe_threshold = 80 # Distance to trigger auto-dismiss + self._swipe_velocity_threshold = 150 # Velocity to trigger dismiss even on shorter swipes self._swipe_in_progress = False + self._current_offset = 0 + self._last_drag_time = 0 + self._drag_velocity = 0 + self._spring_back_duration = 200 # Duration for spring-back animation + self._dismiss_threshold = 0.3 # Dismiss if swiped 30% of width + + # Animation state + self._animation_in_progress = False + self._spring_timer_id = None # Wrap notification in EventBox for swipe detection self.event_box = EventBox( @@ -328,6 +338,131 @@ class NotificationRevealer(SlideRevealer): # Connect our own handler that manages the slide animation self.notification.connect("closed", self.on_resolved) + def _apply_transform(self, offset_x, opacity=1.0, scale=1.0): + """Apply CSS transform to the notification widget for smooth animation""" + if not self.event_box or not self.event_box.get_realized(): + return + + # Clamp values + offset_x = max(0, offset_x) # Only allow rightward movement + opacity = max(0.1, min(1.0, opacity)) # Keep some visibility + scale = max(0.8, min(1.0, scale)) # Subtle scale effect + + # Apply CSS transform + css_transform = f""" + * {{ + transform: translateX({int(offset_x)}px) scale({scale}); + opacity: {opacity}; + transition: none; + }} + """ + + try: + # Apply the transform using GTK's CSS provider + provider = self.event_box.get_style_context().get_property( + "transition", self.event_box.get_state_flags() + ) + self.event_box.get_style_context().add_class("notification-transform") + # Note: In a real implementation, you'd need to set up a CSS provider + # This is a simplified version showing the concept + except: + pass # Fallback if CSS transforms aren't available + + def _animate_spring_back(self, start_offset, target_offset=0, duration=None): + """Animate the notification springing back to its original position""" + if duration is None: + duration = self._spring_back_duration + + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + + start_time = GLib.get_monotonic_time() / 1000 + offset_diff = target_offset - start_offset + + def animate_step(): + current_time = GLib.get_monotonic_time() / 1000 + elapsed = current_time - start_time + progress = min(1.0, elapsed / duration) + + # Use easing function for spring-like animation + eased_progress = self._ease_out_back(progress) + current_offset = start_offset + (offset_diff * eased_progress) + + # Calculate opacity and scale based on offset + max_offset = NOTIFICATION_WIDTH * 0.8 + offset_ratio = abs(current_offset) / max_offset + opacity = 1.0 - (offset_ratio * 0.3) # Fade slightly + scale = 1.0 - (offset_ratio * 0.05) # Slight scale down + + self._apply_transform(current_offset, opacity, scale) + + if progress >= 1.0: + self._animation_in_progress = False + self._current_offset = target_offset + return False + + return True + + self._animation_in_progress = True + self._spring_timer_id = GLib.timeout_add(16, animate_step) # ~60fps + + def _ease_out_back(self, t): + """Easing function for spring-back animation""" + c1 = 1.70158 + c3 = c1 + 1 + return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2) + + def _animate_dismiss(self, start_offset): + """Animate the notification sliding out completely""" + target_offset = NOTIFICATION_WIDTH + 50 # Slide completely out of view + duration = 250 # Quick dismiss animation + + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + + start_time = GLib.get_monotonic_time() / 1000 + offset_diff = target_offset - start_offset + + def animate_step(): + current_time = GLib.get_monotonic_time() / 1000 + elapsed = current_time - start_time + progress = min(1.0, elapsed / duration) + + # Use easing for smooth exit + eased_progress = 1 - pow(1 - progress, 3) # Ease out cubic + current_offset = start_offset + (offset_diff * eased_progress) + + # Fade out and scale down during dismiss + opacity = 1.0 - progress + scale = 1.0 - (progress * 0.2) + + self._apply_transform(current_offset, opacity, scale) + + if progress >= 1.0: + # Trigger the actual notification dismissal + try: + self.notification.close("dismissed-by-user") + except: + pass + return False + + return True + + self._animation_in_progress = True + self._spring_timer_id = GLib.timeout_add(16, animate_step) + + def _calculate_drag_velocity(self, current_x): + """Calculate the velocity of the drag gesture""" + current_time = GLib.get_monotonic_time() / 1000 + + if self._last_drag_time > 0: + time_diff = current_time - self._last_drag_time + if time_diff > 0: + distance_diff = current_x - self._drag_start_x - self._current_offset + self._drag_velocity = abs(distance_diff / time_diff) + + self._last_drag_time = current_time + def _on_animation_complete(self, is_hiding=False): if is_hiding: # Manually destroy the notification widget since we disconnected its handler @@ -347,6 +482,10 @@ class NotificationRevealer(SlideRevealer): self._is_closing = True + # Clean up any ongoing animations + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + # Use different slide directions based on dismiss reason if reason == "expired": # Left-to-right slide for auto-dismiss (expired) @@ -368,6 +507,14 @@ class NotificationRevealer(SlideRevealer): self._drag_start_y = event.y self._is_dragging = True self._swipe_in_progress = False + self._current_offset = 0 + self._last_drag_time = GLib.get_monotonic_time() / 1000 + self._drag_velocity = 0 + + # Stop any ongoing animations + if self._spring_timer_id: + GLib.source_remove(self._spring_timer_id) + self._animation_in_progress = False return False def _on_button_release(self, _widget, event): @@ -383,34 +530,73 @@ class NotificationRevealer(SlideRevealer): elif self._is_dragging and event.button == 1: self._is_dragging = False - - # Calculate swipe distance - dx = event.x - self._drag_start_x - dy = abs(event.y - self._drag_start_y) - - # Right swipe: dismiss current notification (swipe to right to dismiss) - if dx > self._swipe_threshold and dy < 60: # 60px vertical tolerance - try: - self._swipe_in_progress = True - self.notification.close("dismissed-by-user") - logger.debug(f"Notification dismissed by swipe to right: dx={dx}, dy={dy}") - except Exception as e: - logger.error(f"Error dismissing notification by swipe: {e}") - pass - return False - - def _on_motion(self, _widget, event): - if self._is_dragging: - # Calculate current swipe distance for visual feedback + + # Calculate final swipe metrics dx = event.x - self._drag_start_x dy = abs(event.y - self._drag_start_y) - # Optional: Add visual feedback during swipe - # You could add opacity changes or slight translation here - if dx > 20 and dy < 60: # Starting to swipe right - # Visual indication that swipe gesture is recognized - # Could modify opacity or add translation effect here - pass + # Calculate dismiss threshold based on notification width + dismiss_distance = NOTIFICATION_WIDTH * self._dismiss_threshold + + # Determine if we should dismiss or spring back + should_dismiss = ( + (dx > dismiss_distance and dy < 60) or # Dragged far enough + (dx > 30 and self._drag_velocity > self._swipe_velocity_threshold and dy < 60) # Fast swipe + ) + + if should_dismiss: + try: + self._swipe_in_progress = True + # Animate the dismiss + self._animate_dismiss(self._current_offset) + logger.debug(f"Notification dismissed by swipe: dx={dx}, velocity={self._drag_velocity}") + except Exception as e: + logger.error(f"Error dismissing notification by swipe: {e}") + else: + # Spring back to original position + if abs(self._current_offset) > 5: # Only animate if there's noticeable displacement + self._animate_spring_back(self._current_offset, 0) + else: + self._apply_transform(0, 1.0, 1.0) + self._current_offset = 0 + return False + + def _on_motion(self, _widget, event): + if self._is_dragging and not self._animation_in_progress: + # Calculate current swipe distance + dx = event.x - self._drag_start_x + dy = abs(event.y - self._drag_start_y) + + # Only respond to primarily horizontal gestures + if dy < 60: # Vertical tolerance + # Calculate velocity for smooth interaction + self._calculate_drag_velocity(event.x) + + # Apply real-time transform - only allow rightward movement + if dx > 0: + self._current_offset = dx + + # Calculate visual feedback based on drag distance + max_offset = NOTIFICATION_WIDTH * 0.8 + offset_ratio = min(1.0, dx / max_offset) + + # Apply diminishing returns for large swipes + adjusted_offset = dx * (1.0 - offset_ratio * 0.3) + + # Calculate opacity and scale + opacity = 1.0 - (offset_ratio * 0.4) # Fade as it's dragged + scale = 1.0 - (offset_ratio * 0.1) # Slight scale down + + self._apply_transform(adjusted_offset, opacity, scale) + + # Visual indication of dismiss threshold + if dx > NOTIFICATION_WIDTH * self._dismiss_threshold: + # Could add visual cue here (like changing color) + pass + else: + # Reset to original position if dragging left + self._current_offset = 0 + self._apply_transform(0, 1.0, 1.0) return False