Notification & misc enhancements #13
No files matched your search
@@ -13,3 +13,4 @@ config/hypr/colors.conf
|
||||
fabric/**
|
||||
fabric/
|
||||
# styles/colors.css
|
||||
.aider*
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="-10 -10 45 45" width="28" height="28" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
|
||||
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline>
|
||||
<svg width="24" height="24" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m11.5 5c-0.277 0-0.5 0.223-0.5 0.5v4.875l-1.719-1.5c-0.312-0.273-0.757-0.22-1.03 0.094a0.753 0.753 0 0 0 0.062 1.062l2.25 1.969-2.25 1.969a0.753 0.753 0 0 0-0.063 1.062c0.274 0.313 0.719 0.367 1.031 0.094l1.72-1.5v4.875c0 0.277 0.222 0.5 0.5 0.5 0.282 0 0.437-0.02 0.655-0.188l3.532-3.156a0.78 0.78 0 0 0 0.218-0.375 0.745 0.745 0 0 0 0.032-0.281 0.723 0.723 0 0 0-0.032-0.156 0.78 0.78 0 0 0-0.218-0.375l-2.813-2.47 2.813-2.468a0.78 0.78 0 0 0 0.218-0.375 0.712 0.712 0 0 0 0-0.437 0.78 0.78 0 0 0-0.218-0.375l-3.532-3.157c-0.199-0.16-0.427-0.187-0.656-0.187zm0.5 2.063 2.094 1.875-2.094 1.812zm0 6.187 2.094 1.813-2.094 1.875z" color="#bebebe" fill="#f2f2f2" overflow="visible"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 777 B |
@@ -1,4 +1,30 @@
|
||||
<svg viewBox="-10 -10 45 45" width="28" height="28" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
|
||||
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline>
|
||||
<line x1="2" y1="2" x2="22" y2="22" stroke="#fff" stroke-width="2"></line>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg6"
|
||||
version="1.1"
|
||||
width="24"
|
||||
height="24">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<path
|
||||
id="path2"
|
||||
d="M 6.3515625,4.7363281 5.4121094,5.6777344 17.601562,17.867188 18.541016,16.925781 13.267578,11.652344 15.6875,9.53125 c 0.106935,-0.1017801 0.18279,-0.2318176 0.21875,-0.375 0.04605,-0.1421978 0.04605,-0.2953022 0,-0.4375 C 15.87029,8.5755676 15.794435,8.4455301 15.6875,8.34375 L 12.15625,5.1875 C 11.95725,5.0275 11.729,5 11.5,5 11.223,5 11,5.223 11,5.5 V 9.3847656 Z M 12,7.0625 14.09375,8.9375 12.195312,10.580078 12,10.384766 Z M 10.494141,12.060547 8.3125,13.96875 c -0.310445,0.276247 -0.3384165,0.751761 -0.0625,1.0625 0.273,0.313 0.71825,0.36675 1.03125,0.09375 l 1.720703,-1.5 V 18.5 c 0,0.277 0.222,0.5 0.5,0.5 0.282,0 0.436297,-0.0195 0.654297,-0.1875 L 14.84375,16.410156 13.779297,15.345703 12,16.9375 v -3.371094 z"
|
||||
style="color:#bebebe;overflow:visible;fill:#f2f2f2;marker:none" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg viewBox="-4 -4 32 32" width="24" height="24" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
||||
<svg width="24" height="24" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style id="current-color-scheme" type="text/css">.ColorScheme-Text { color:#dedede; }</style>
|
||||
</defs>
|
||||
<path class="ColorScheme-Text" d="m12 7.0206c-2.6432 0-5.1397 1.1665-6.85 3.2009a0.63845 0.63845 0 0 0 0.49005 1.0491c0.18222 0 0.36305-0.0773 0.48971-0.22742 1.4657-1.7438 3.6057-2.7446 5.8703-2.7446 2.2646 0 4.4046 1.0012 5.8703 2.7446 0.12631 0.15116 0.30714 0.22777 0.48971 0.22777a0.63776 0.63776 0 0 0 0.4897-1.0495c-1.7097-2.0344-4.2068-3.2009-6.8504-3.2009zm0 2.707c-1.8877 0-3.6709 0.83378-4.8922 2.2863a0.63894 0.63894 0 1 0 0.97838 0.82204 5.1055 5.1055 0 0 1 3.9135-1.8301c1.5102 0 2.9369 0.66744 3.9142 1.8301 0.12631 0.15116 0.30784 0.22777 0.48971 0.22777a0.63845 0.63845 0 0 0 0.48833-1.0495c-1.2217-1.4529-3.0038-2.2867-4.8922-2.2867zm0 2.8126a3.8272 3.8272 0 0 0-2.9358 1.3704 0.63845 0.63845 0 0 0 0.079374 0.90038 0.63603 0.63603 0 0 0 0.90038-0.07765 2.5507 2.5507 0 0 1 1.9561-0.91453c0.75475 0 1.4677 0.33303 1.9568 0.91453a0.63707 0.63707 0 0 0 0.90004 0.07765 0.63845 0.63845 0 0 0 0.07937-0.90038 3.8272 3.8272 0 0 0-2.9362-1.3704zm-0.05452 2.4264c-0.49971 0.04832-0.98079 0.42172-0.99736 0.97838a1.0522 1.0522 0 0 0 2.1041 0c-0.08869-0.74716-0.6067-1.026-1.1068-0.97838z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg viewBox="-10 -10 45 45" width="28" height="28" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
|
||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
|
||||
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
|
||||
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
|
||||
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
||||
<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#dedede" opacity=".35">
|
||||
<path d="m8 3c-2.8405-3e-7 -5.4324 1.0071-7.4863 2.6582a0.5 0.5 0 0 0-0.095703 0.072266 0.5 0.5 0 0 0-0.1543 0.35938 0.5 0.5 0 0 0 0.5 0.5 0.5 0.5 0 0 0 0.31836-0.11328c1.891-1.5388 4.2898-2.4766 6.918-2.4766 2.6282-3e-7 5.0269 0.93776 6.918 2.4766a0.5 0.5 0 0 0 0.31836 0.11328 0.5 0.5 0 0 0 0.5-0.5 0.5 0.5 0 0 0-0.1543-0.35938 0.5 0.5 0 0 0-0.0957-0.072266c-2.0539-1.6511-4.6459-2.6582-7.4863-2.6582z" fill-rule="evenodd" style="paint-order:stroke fill markers"/>
|
||||
<path d="m8 9c-1.3525-2e-7 -2.5832 0.46891-3.5859 1.2246-0.11034 0.09497-0.16795 0.23918-0.16797 0.38477 0 0.27614 0.22386 0.5 0.5 0.5 0.12436-3.26e-4 0.24413-0.04698 0.33594-0.13086 0.82215-0.60306 1.8199-0.97852 2.918-0.97852 1.0981-2e-7 2.0958 0.37546 2.918 0.97852 0.09181 0.08388 0.21158 0.13053 0.33594 0.13086 0.27614 0 0.5-0.22386 0.5-0.5-1.6e-5 -0.14559-0.05762-0.2898-0.16797-0.38477-1.0027-0.7557-2.2334-1.2246-3.5859-1.2246z"/>
|
||||
<path d="m8 6c-2.0731 0-3.9681 0.71726-5.4883 1.8965a0.5 0.5 0 0 0-0.10156 0.074219 0.5 0.5 0 0 0-0.15625 0.36133 0.5 0.5 0 0 0 0.5 0.5 0.5 0.5 0 0 0 0.32031-0.11523c1.3588-1.0688 3.0627-1.7168 4.9258-1.7168 1.8631-2e-7 3.567 0.64796 4.9258 1.7168a0.5 0.5 0 0 0 0.32031 0.11523 0.5 0.5 0 0 0 0.5-0.5 0.5 0.5 0 0 0-0.15625-0.36133 0.5 0.5 0 0 0-0.10156-0.074219c-1.5202-1.1792-3.4152-1.8965-5.4883-1.8965z" fill-rule="evenodd" style="paint-order:stroke fill markers"/>
|
||||
<circle cx="8" cy="13" r="1" fill-rule="evenodd" style="paint-order:stroke fill markers"/>
|
||||
</g>
|
||||
<rect transform="rotate(45)" x="1.3137" y="-.5" width="20" height="1" rx=".5" ry=".5" fill="#dedede" stroke-linecap="round" stroke-linejoin="round" stroke-width=".48307" style="paint-order:markers stroke fill"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 519 B After Width: | Height: | Size: 1.8 KiB |
@@ -24,7 +24,8 @@ NOTIFICATION_WIDTH = 360
|
||||
NOTIFICATION_IMAGE_SIZE = 48
|
||||
|
||||
|
||||
def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 600):
|
||||
def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 350):
|
||||
"""Configure revealer for macOS-like smooth animation"""
|
||||
revealer.duration = duration
|
||||
|
||||
|
||||
@@ -67,6 +68,8 @@ class NotificationWidget(Box):
|
||||
**kwargs,
|
||||
):
|
||||
self.show_close_button = show_close_button
|
||||
self.close_button = None
|
||||
self._is_hovered = False
|
||||
|
||||
super().__init__(
|
||||
size=(NOTIFICATION_WIDTH, -1),
|
||||
@@ -83,6 +86,11 @@ class NotificationWidget(Box):
|
||||
self.notification = notification
|
||||
self.timeout_ms = timeout_ms
|
||||
self._timeout_id = None
|
||||
|
||||
# Add hover events to the main notification widget
|
||||
self.connect("enter-notify-event", self._on_enter_notify)
|
||||
self.connect("leave-notify-event", self._on_leave_notify)
|
||||
|
||||
self.start_timeout()
|
||||
|
||||
def create_header(self, notification):
|
||||
@@ -182,6 +190,30 @@ class NotificationWidget(Box):
|
||||
],
|
||||
)
|
||||
|
||||
def create_close_button(self):
|
||||
if self.close_button is None:
|
||||
self.close_button = Button(
|
||||
name="notification-close-button",
|
||||
visible=False, # Initially hidden
|
||||
on_clicked=lambda *_: self.notification.close("dismissed-by-user"),
|
||||
child=Label(label="×", name="close-button-label"),
|
||||
)
|
||||
return self.close_button
|
||||
|
||||
def _on_enter_notify(self, widget, event):
|
||||
self._is_hovered = True
|
||||
if self.close_button:
|
||||
self.close_button.set_visible(True)
|
||||
self.pause_timeout()
|
||||
return False
|
||||
|
||||
def _on_leave_notify(self, widget, event):
|
||||
self._is_hovered = False
|
||||
if self.close_button:
|
||||
self.close_button.set_visible(False)
|
||||
self.resume_timeout()
|
||||
return False
|
||||
|
||||
def get_pixbuf(self, icon_path, width, height):
|
||||
if icon_path.startswith("file://"):
|
||||
icon_path = icon_path[7:]
|
||||
@@ -226,6 +258,7 @@ class NotificationWidget(Box):
|
||||
self.stop_timeout()
|
||||
|
||||
def resume_timeout(self):
|
||||
if not self._is_hovered: # Only resume if not hovered
|
||||
self.start_timeout()
|
||||
|
||||
def destroy(self):
|
||||
@@ -244,7 +277,7 @@ class NotificationWidget(Box):
|
||||
self.set_pointer_cursor(button, "hand2")
|
||||
|
||||
def unhover_button(self, button):
|
||||
self.resume_timeout()
|
||||
# Don't resume timeout here since the notification itself might still be hovered
|
||||
self.set_pointer_cursor(button, "arrow")
|
||||
|
||||
|
||||
@@ -259,8 +292,9 @@ class NotificationRevealer(SlideRevealer):
|
||||
self.notif_box = NotificationWidget(notification, show_close_button=False)
|
||||
self.notification = notification
|
||||
self.on_transition_end = on_transition_end
|
||||
# Reference to ModusNoti window for queue clearing
|
||||
# Reference to NotificationCenter window for queue clearing
|
||||
self.parent_window = parent_window
|
||||
self._is_closing = False
|
||||
|
||||
# Add swipe detection variables
|
||||
self._drag_start_x = 0
|
||||
@@ -285,7 +319,7 @@ class NotificationRevealer(SlideRevealer):
|
||||
super().__init__(
|
||||
child=self.event_box,
|
||||
direction="right",
|
||||
duration=600,
|
||||
duration=350, # Optimized duration for macOS-like feel
|
||||
)
|
||||
|
||||
smooth_revealer_animation(self)
|
||||
@@ -307,6 +341,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
|
||||
@@ -316,7 +355,8 @@ class NotificationRevealer(SlideRevealer):
|
||||
self.set_slide_direction("right")
|
||||
|
||||
self.hide()
|
||||
GLib.timeout_add(self.duration + 50, lambda: self._on_animation_complete(True))
|
||||
# Reduced timeout for snappier transitions
|
||||
GLib.timeout_add(self.duration + 30, lambda: self._on_animation_complete(True))
|
||||
|
||||
def _on_button_press(self, _widget, event):
|
||||
if event.button == 1:
|
||||
@@ -359,6 +399,12 @@ class NotificationRevealer(SlideRevealer):
|
||||
return False
|
||||
|
||||
|
||||
class NotificationState:
|
||||
IDLE = 0
|
||||
SHOWING = 1
|
||||
HIDING = 2
|
||||
|
||||
|
||||
class ModusNoti(Window):
|
||||
def __init__(self):
|
||||
self._server = notification_service
|
||||
@@ -371,10 +417,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
|
||||
@@ -409,6 +456,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")
|
||||
@@ -416,46 +464,64 @@ 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
|
||||
# Add new notification to queue
|
||||
self.notification_queue.append(notification)
|
||||
|
||||
if hasattr(self.current_notification, "stop_animation"):
|
||||
self.current_notification.stop_animation()
|
||||
# Process the queue
|
||||
self._process_notification_queue()
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
self.notification_queue.append(notification)
|
||||
self.show_next_notification()
|
||||
|
||||
def show_next_notification(self):
|
||||
if not self.notification_queue or self.is_showing_notification:
|
||||
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)
|
||||
@@ -463,37 +529,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():
|
||||
if new_box.get_parent() and new_box.get_realized():
|
||||
new_box.reveal()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
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:
|
||||
GLib.timeout_add(100, 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)
|
||||
@@ -505,5 +585,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)
|
||||
@@ -15,7 +15,7 @@ class BluetoothIndicator(Box):
|
||||
self.bluetooth = BluetoothClient()
|
||||
self.bt_icon = Svg(
|
||||
name="bt-icon",
|
||||
size=25,
|
||||
size=23,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
),
|
||||
@@ -111,7 +111,7 @@ class NetworkIndicator(Box):
|
||||
|
||||
self.network_icon = Svg(
|
||||
name="network-icon",
|
||||
size=22,
|
||||
size=25,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/wifi-clear.svg"
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class Panel(Window):
|
||||
self.imac = Button(
|
||||
name="panel-button",
|
||||
child=Svg(
|
||||
size=28,
|
||||
size=23,
|
||||
svg_file=get_relative_path("../../config/assets/icons/misc/logo.svg"),
|
||||
),
|
||||
on_clicked=lambda *_: self.menubar.show_system_dropdown(self.imac),
|
||||
@@ -128,7 +128,7 @@ class Panel(Window):
|
||||
self.datetime_button = Button(
|
||||
name="datetime-button",
|
||||
on_clicked=self.notification_center.toggle_mousecapture,
|
||||
child=DateTime(name="date-time", formatters=["%a %b %d %I:%M"]),
|
||||
child=DateTime(name="date-time", formatters=["%a %b %d %I:%M %P"]),
|
||||
)
|
||||
|
||||
self.recording_indicator = RecordingIndicator()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#about-menu {
|
||||
background-color: alpha(#001, 0.3);
|
||||
background-color: alpha(#000, 0.34);
|
||||
|
||||
box-shadow: inset 0 -0.5px 0 .5px alpha(#555, 0.7), inset 0 0.5px 0 .5px alpha(#777, 0.7);
|
||||
border: 1px solid alpha(#111, 0.3);
|
||||
border-top: none;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#noti-center-box {
|
||||
background-color: alpha(#001, 0.3);
|
||||
background-color: alpha(#fff, 0.09);
|
||||
|
||||
border: 1px solid alpha(#111, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#osd {
|
||||
background-color: alpha(#000, 0.03);
|
||||
background-color: alpha(#fff, 0.09);
|
||||
padding: 12px 20px;
|
||||
margin: 70px;
|
||||
min-height: 200px;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
#panel {
|
||||
background-color: alpha(#010101, 0.1);
|
||||
background-color: alpha(#fff, 0.05);
|
||||
|
||||
border-bottom: 1px solid alpha(#010101, 0.025);
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
transition: all 120ms ease-in-out;
|
||||
}
|
||||
|
||||
#panel-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
#global-title-button{
|
||||
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#panel-button {
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from gi.repository import GLib, Gtk
|
||||
import gi
|
||||
import math
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
@@ -21,7 +22,9 @@ class AnimationManager:
|
||||
def add_widget(self, widget):
|
||||
self._animating_widgets.add(widget)
|
||||
if self._timer_id is None:
|
||||
self._timer_id = GLib.timeout_add(16, self._animate_all) # 60 FPS
|
||||
# Use 120 FPS for ultra-smooth animations like macOS
|
||||
self._timer_id = GLib.timeout_add(8, self._animate_all) # 120 FPS
|
||||
|
||||
|
||||
def remove_widget(self, widget):
|
||||
self._animating_widgets.discard(widget)
|
||||
@@ -37,6 +40,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)
|
||||
@@ -45,11 +50,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()
|
||||
|
||||
@@ -59,45 +64,76 @@ 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):
|
||||
widget_count = len(self._animating_widgets)
|
||||
if widget_count <= 1:
|
||||
return 16 # 60 FPS
|
||||
elif widget_count <= 2:
|
||||
return 16 # 60 FPS
|
||||
elif widget_count <= 4:
|
||||
return 22 # 45 FPS
|
||||
elif widget_count <= 6:
|
||||
return 33 # 30 FPS
|
||||
else:
|
||||
return 50 # 20 FPS
|
||||
"""Keep consistent 120 FPS for macOS-like smoothness"""
|
||||
return 8 # 120 FPS
|
||||
|
||||
|
||||
def _start_timer(self):
|
||||
interval = self._get_optimal_interval()
|
||||
self._timer_id = GLib.timeout_add(interval, self._animate_all)
|
||||
|
||||
def _adjust_frame_rate(self):
|
||||
if not self._timer_id:
|
||||
return
|
||||
# No need to adjust frame rate anymore - keep it consistent
|
||||
pass
|
||||
|
||||
new_interval = self._get_optimal_interval()
|
||||
GLib.source_remove(self._timer_id)
|
||||
self._timer_id = GLib.timeout_add(new_interval, self._animate_all)
|
||||
|
||||
class MacOSEasing:
|
||||
"""macOS-style easing functions for natural motion"""
|
||||
|
||||
@staticmethod
|
||||
def ease_out_expo(t):
|
||||
"""Exponential ease out - fast start, slow end"""
|
||||
return 1 - math.pow(2, -10 * t) if t != 1 else 1
|
||||
|
||||
@staticmethod
|
||||
def ease_in_out_quart(t):
|
||||
"""Quartic ease in-out for smooth acceleration/deceleration"""
|
||||
return 8 * t * t * t * t if t < 0.5 else 1 - math.pow(-2 * t + 2, 4) / 2
|
||||
|
||||
@staticmethod
|
||||
def ease_out_back(t):
|
||||
"""Back ease out for slight overshoot effect"""
|
||||
c1 = 1.70158
|
||||
c3 = c1 + 1
|
||||
return 1 + c3 * math.pow(t - 1, 3) + c1 * math.pow(t - 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def ease_out_cubic_bezier(t):
|
||||
"""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):
|
||||
def __init__(self, child: Gtk.Widget, direction="right", duration=300, size=None):
|
||||
def __init__(self, child: Gtk.Widget, direction="right", duration=350, size=None):
|
||||
super().__init__()
|
||||
|
||||
self.child = child
|
||||
self.direction = direction
|
||||
self.duration = duration
|
||||
self.duration = duration # Slightly faster for snappier feel
|
||||
self.fixed_size = size
|
||||
self._revealed = False
|
||||
self._animating = False
|
||||
self._start_time = None
|
||||
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)
|
||||
@@ -129,7 +165,7 @@ 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
|
||||
|
||||
@@ -144,36 +180,33 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self.connect("realize", on_realize)
|
||||
|
||||
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)
|
||||
|
||||
# Force layout update to get accurate dimensions
|
||||
if show:
|
||||
self.child.show()
|
||||
self.queue_resize()
|
||||
|
||||
# Wait for layout to complete before getting dimensions
|
||||
def start_with_dimensions():
|
||||
self._cached_dimensions = self._get_dimensions()
|
||||
if self._cached_dimensions[0] == 0 or self._cached_dimensions[1] == 0:
|
||||
self._animating = False
|
||||
return 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._fixed.move(self.child, *self._get_offscreen_pos_cached())
|
||||
self.child.show()
|
||||
pos = self._get_offscreen_pos_cached()
|
||||
self._current_position = (float(pos[0]), float(pos[1]))
|
||||
self._fixed.move(self.child, int(pos[0]), int(pos[1]))
|
||||
|
||||
AnimationManager.get_instance().add_widget(self)
|
||||
return False
|
||||
|
||||
# Use idle_add to ensure layout is complete
|
||||
GLib.idle_add(start_with_dimensions)
|
||||
@@ -185,17 +218,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:
|
||||
# Ease out quadratic (no bounce/overshoot)
|
||||
eased = 1 - (1 - t) ** 2
|
||||
# Use quintic ease out for very smooth revealing
|
||||
eased = MacOSEasing.ease_out_quint(t)
|
||||
else:
|
||||
eased = t**2 # Ease in quadratic for hide animation
|
||||
# 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
|
||||
@@ -204,7 +240,11 @@ class SlideRevealer(Gtk.Overlay):
|
||||
def _apply_position(self):
|
||||
if self._pending_position:
|
||||
x, y = self._pending_position
|
||||
self._fixed.move(self.child, x, y)
|
||||
# Use sub-pixel positioning for smoother motion
|
||||
self._current_position = (x, y)
|
||||
# Round to nearest pixel for actual positioning
|
||||
pixel_x, pixel_y = int(round(x)), int(round(y))
|
||||
self._fixed.move(self.child, pixel_x, pixel_y)
|
||||
self._pending_position = None
|
||||
|
||||
def _get_container_for_redraw(self):
|
||||
@@ -234,24 +274,24 @@ class SlideRevealer(Gtk.Overlay):
|
||||
if self._show_animation:
|
||||
# Showing animation: slide from offscreen to onscreen (0,0)
|
||||
if self.direction == "left":
|
||||
return int(-w + w * progress), 0
|
||||
return -w + w * progress, 0.0
|
||||
elif self.direction == "right":
|
||||
return int(w - w * progress), 0
|
||||
return w - w * progress, 0.0
|
||||
elif self.direction == "top":
|
||||
return 0, int(-h + h * progress)
|
||||
return 0.0, -h + h * progress
|
||||
elif self.direction == "bottom":
|
||||
return 0, int(h - h * progress)
|
||||
return 0.0, h - h * progress
|
||||
else:
|
||||
# Hiding animation: slide from onscreen (0,0) to offscreen
|
||||
if self.direction == "left":
|
||||
return int(-w * progress), 0 # Slide left (negative x)
|
||||
return -w * progress, 0.0 # Slide left (negative x)
|
||||
elif self.direction == "right":
|
||||
return int(w * progress), 0 # Slide right (positive x)
|
||||
return w * progress, 0.0 # Slide right (positive x)
|
||||
elif self.direction == "top":
|
||||
return 0, int(-h * progress) # Slide up (negative y)
|
||||
return 0.0, -h * progress # Slide up (negative y)
|
||||
elif self.direction == "bottom":
|
||||
return 0, int(h * progress) # Slide down (positive y)
|
||||
return 0, 0
|
||||
return 0.0, h * progress # Slide down (positive y)
|
||||
return 0.0, 0.0
|
||||
|
||||
def set_slide_direction(self, direction):
|
||||
self.direction = direction
|
||||
@@ -270,6 +310,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()
|
||||
|
||||