🔧 - Added Notifications component with dock and system tray indicators
This commit is contained in:
10 files changed
+937
-86
No files matched your search
@@ -80,6 +80,7 @@ if os.path.exists(CONFIG_FILE):
|
||||
"date_time": config.get("dock_date_time_visible", True),
|
||||
"controls": config.get("dock_controls_visible", True),
|
||||
"indicators": config.get("dock_indicators_visible", True),
|
||||
"notifications": config.get("dock_notifications_visible", True),
|
||||
"systray": config.get("dock_tray_visible", True),
|
||||
"applications": config.get("dock_applications_visible", True),
|
||||
"music_player": config.get("dock_music_player_visible", True),
|
||||
@@ -111,6 +112,7 @@ else:
|
||||
"date_time": True,
|
||||
"controls": True,
|
||||
"indicators": True,
|
||||
"notifications": True,
|
||||
"systray": True,
|
||||
"applications": True,
|
||||
"music_player": True,
|
||||
|
||||
@@ -54,6 +54,7 @@ DEFAULTS = {
|
||||
"dock_date_time_visible": True,
|
||||
"dock_controls_visible": True,
|
||||
"dock_indicators_visible": True,
|
||||
"dock_notifications_visible": True,
|
||||
"dock_applications_visible": True,
|
||||
"dock_language_visible": True,
|
||||
'dock_hide_special_workspace': True,
|
||||
|
||||
@@ -19,6 +19,7 @@ from .controls import Controls
|
||||
from .indicators import Indicators
|
||||
from .metrics import Metrics
|
||||
from .music_player import MusicPlayer
|
||||
from .notifications import Notifications
|
||||
from .workspaces import workspace
|
||||
|
||||
import gi
|
||||
@@ -111,6 +112,7 @@ class DockComponents(Box):
|
||||
)
|
||||
self.controls = Controls()
|
||||
self.indicators = Indicators()
|
||||
self.notifications = Notifications()
|
||||
self.music_player = MusicPlayer()
|
||||
self.systray = SystemTray(
|
||||
icon_size=18,
|
||||
@@ -126,6 +128,7 @@ class DockComponents(Box):
|
||||
self.controls,
|
||||
self.applications,
|
||||
self.indicators,
|
||||
self.notifications,
|
||||
self.battery,
|
||||
self.music_player,
|
||||
self.date_time,
|
||||
@@ -158,6 +161,7 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"notifications": self.notifications,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
@@ -173,6 +177,11 @@ class DockComponents(Box):
|
||||
widget._update_visibility_based_on_content()
|
||||
else:
|
||||
widget.set_visible(self.component_visibility[component_name])
|
||||
# Special handling for music player - let it manage its own visibility based on media state
|
||||
elif component_name == "music_player":
|
||||
# Music player manages its own visibility based on config and media state
|
||||
# Don't override its visibility here, let it handle it internally
|
||||
pass
|
||||
else:
|
||||
widget.set_visible(self.component_visibility[component_name])
|
||||
|
||||
@@ -184,6 +193,7 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"notifications": self.notifications,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
@@ -194,9 +204,18 @@ class DockComponents(Box):
|
||||
self.component_visibility[component_name] = not self.component_visibility[
|
||||
component_name
|
||||
]
|
||||
components[component_name].set_visible(
|
||||
self.component_visibility[component_name]
|
||||
)
|
||||
|
||||
# Special handling for music player - update the global config and let it handle its own visibility
|
||||
if component_name == "music_player":
|
||||
# Update the global config data
|
||||
import config.data as data
|
||||
data.DOCK_COMPONENTS_VISIBILITY[component_name] = self.component_visibility[component_name]
|
||||
# Let the music player update its own visibility based on the new config
|
||||
components[component_name]._update_display()
|
||||
else:
|
||||
components[component_name].set_visible(
|
||||
self.component_visibility[component_name]
|
||||
)
|
||||
|
||||
config_file = get_relative_path("../../../config/assets/config.json")
|
||||
if os.path.exists(config_file):
|
||||
|
||||
@@ -51,7 +51,14 @@ class MusicPlayer(EventBox):
|
||||
self._create_music_player_view()
|
||||
self.add(self.main_container)
|
||||
self.connect("button-press-event", self._on_music_player_clicked)
|
||||
self.set_visible(False)
|
||||
|
||||
# Set initial visibility based on configuration
|
||||
initial_visibility = data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True)
|
||||
self.set_visible(initial_visibility)
|
||||
|
||||
# Ensure visualizer is disabled in vertical mode
|
||||
if data.VERTICAL:
|
||||
self._show_visualizer = False
|
||||
|
||||
GLib.timeout_add(100, self._delayed_initial_update)
|
||||
|
||||
@@ -60,7 +67,7 @@ class MusicPlayer(EventBox):
|
||||
|
||||
self.album_thumbnail = CircleImage(
|
||||
name="music-album-thumbnail",
|
||||
size=34,
|
||||
size=30,
|
||||
v_align="center",
|
||||
visible=True,
|
||||
)
|
||||
@@ -68,13 +75,21 @@ class MusicPlayer(EventBox):
|
||||
self.album_thumbnail.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
|
||||
self.album_thumbnail.connect("button-press-event", self._on_thumbnail_clicked)
|
||||
|
||||
# Create track label (hidden in vertical mode)
|
||||
self.track_label = Label(
|
||||
name="music-track",
|
||||
label="No Media",
|
||||
ellipsize=0,
|
||||
h_align="center",
|
||||
)
|
||||
self.track_label.set_size_request(100, -1)
|
||||
|
||||
if data.VERTICAL:
|
||||
self.track_label.set_visible(False) # Hide label in vertical mode
|
||||
self.track_label.set_size_request(-1, -1)
|
||||
else:
|
||||
self.track_label.set_visible(True)
|
||||
self.track_label.set_size_request(100, -1)
|
||||
|
||||
self.track_label.set_hexpand(False)
|
||||
self.track_label.set_vexpand(False)
|
||||
|
||||
@@ -105,6 +120,11 @@ class MusicPlayer(EventBox):
|
||||
end_children=self.controls_box,
|
||||
)
|
||||
|
||||
# Add vertical class for CSS styling
|
||||
if data.VERTICAL:
|
||||
self.music_player_box.add_style_class("vertical")
|
||||
self.main_container.add_style_class("vertical")
|
||||
|
||||
self.main_container.add(self.music_player_box)
|
||||
|
||||
def _load_album_art(self, art_url):
|
||||
@@ -343,7 +363,19 @@ class MusicPlayer(EventBox):
|
||||
self._connect_player_signals(self._current_player)
|
||||
|
||||
if not self._current_player:
|
||||
self.set_visible(False)
|
||||
# Only hide if the component is disabled in settings
|
||||
# If enabled in settings, show a "No Media" state instead of hiding
|
||||
if not data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True):
|
||||
self.set_visible(False)
|
||||
else:
|
||||
# Show "No Media" state when enabled but no player is active
|
||||
self.set_visible(True)
|
||||
# Clear tooltips when no player is active
|
||||
self.set_tooltip_text("No media player active")
|
||||
self.album_thumbnail.set_tooltip_text("No media player active")
|
||||
if not data.VERTICAL:
|
||||
self.track_label.set_tooltip_text("No media player active")
|
||||
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
self._update_timeout_id = None
|
||||
@@ -353,7 +385,12 @@ class MusicPlayer(EventBox):
|
||||
self._stop_spin_animation()
|
||||
return False
|
||||
|
||||
self.set_visible(True)
|
||||
# Only show if enabled in configuration
|
||||
if data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True):
|
||||
self.set_visible(True)
|
||||
else:
|
||||
self.set_visible(False)
|
||||
return False
|
||||
|
||||
album_art_url = self._current_player.album_image_url
|
||||
if album_art_url:
|
||||
@@ -370,18 +407,20 @@ class MusicPlayer(EventBox):
|
||||
else:
|
||||
play_pause_icon.set_markup(icons.play)
|
||||
|
||||
track_title = self._current_player.track_title or "Unknown Track"
|
||||
max_display_chars = 12
|
||||
# Skip text processing in vertical mode since label is hidden
|
||||
if not data.VERTICAL:
|
||||
track_title = self._current_player.track_title or "Unknown Track"
|
||||
max_display_chars = 12
|
||||
|
||||
self._original_title = track_title
|
||||
self._title_needs_marquee = len(track_title) > max_display_chars
|
||||
self._original_title = track_title
|
||||
self._title_needs_marquee = len(track_title) > max_display_chars
|
||||
|
||||
if self._original_title != getattr(self, "_last_original_title", ""):
|
||||
self._last_original_title = self._original_title
|
||||
self._marquee_position = 0
|
||||
self._start_marquee_animation()
|
||||
if self._original_title != getattr(self, "_last_original_title", ""):
|
||||
self._last_original_title = self._original_title
|
||||
self._marquee_position = 0
|
||||
self._start_marquee_animation()
|
||||
|
||||
self._update_marquee_text()
|
||||
self._update_marquee_text()
|
||||
self._update_tooltips()
|
||||
|
||||
self.prev_button.set_sensitive(self._current_player.can_go_previous)
|
||||
@@ -420,6 +459,11 @@ class MusicPlayer(EventBox):
|
||||
if not self._current_player:
|
||||
return
|
||||
|
||||
# Skip updating label text in vertical mode since it's hidden
|
||||
if data.VERTICAL:
|
||||
return
|
||||
|
||||
# Use the same character limit as in _update_display
|
||||
max_display_chars = 12
|
||||
|
||||
if self._title_needs_marquee:
|
||||
@@ -465,7 +509,14 @@ class MusicPlayer(EventBox):
|
||||
player_name = self._current_player.player_name or "Media Player"
|
||||
|
||||
tooltip_text = f"{full_track_title}\nby {track_artist}\n\nPlayer: {player_name}"
|
||||
self.track_label.set_tooltip_text(tooltip_text)
|
||||
|
||||
if data.VERTICAL:
|
||||
# In vertical mode, show tooltip on the main container and album thumbnail
|
||||
self.set_tooltip_text(tooltip_text)
|
||||
self.album_thumbnail.set_tooltip_text(tooltip_text)
|
||||
else:
|
||||
# In horizontal mode, show tooltip on the track label
|
||||
self.track_label.set_tooltip_text(tooltip_text)
|
||||
|
||||
def _on_play_pause_clicked(self, _button):
|
||||
if self._current_player:
|
||||
@@ -485,13 +536,15 @@ class MusicPlayer(EventBox):
|
||||
self._update_display()
|
||||
|
||||
def _on_thumbnail_clicked(self, _widget, _event):
|
||||
if self._current_player:
|
||||
if self._current_player and not data.VERTICAL:
|
||||
# Only toggle visualizer in horizontal mode
|
||||
self._show_visualizer = not self._show_visualizer
|
||||
self._update_stack_display()
|
||||
return True
|
||||
|
||||
def _on_music_player_clicked(self, _widget, _event):
|
||||
if self._current_player:
|
||||
if self._current_player and not data.VERTICAL:
|
||||
# Only toggle visualizer in horizontal mode
|
||||
self._show_visualizer = not self._show_visualizer
|
||||
self._update_stack_display()
|
||||
return True
|
||||
@@ -511,7 +564,8 @@ class MusicPlayer(EventBox):
|
||||
self.cavalcade = None
|
||||
|
||||
def _update_stack_display(self):
|
||||
if self._show_visualizer and self._current_player:
|
||||
# Never show visualizer in vertical mode
|
||||
if self._show_visualizer and self._current_player and not data.VERTICAL:
|
||||
self._create_visualizer_if_needed()
|
||||
if self.cavalcade_box:
|
||||
self.cavalcade_box.set_visible(True)
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from services import notification_service
|
||||
from modules.notification_popup import NotificationWidget
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
class NotificationIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-notifications", **kwargs)
|
||||
|
||||
self.notification_service = notification_service
|
||||
self.notification_count = 0
|
||||
|
||||
# Create the notification icon (switches between normal and dots)
|
||||
self.notification_icon = Label(
|
||||
name="notification-icon",
|
||||
markup=icons.notifications
|
||||
)
|
||||
|
||||
# Add the icon directly (no overlay needed)
|
||||
self.add(self.notification_icon)
|
||||
|
||||
# Connect to notification service signals
|
||||
self.notification_service.connect("notification_count", self.on_notification_count_changed)
|
||||
self.notification_service.connect("notification-added", self.on_notification_added)
|
||||
self.notification_service.connect("dnd", self.on_dnd_changed)
|
||||
|
||||
# Connect click handler
|
||||
self.connect("clicked", self.on_clicked)
|
||||
|
||||
# Initialize count
|
||||
self.update_count()
|
||||
|
||||
# Popup window for showing notifications
|
||||
self.popup_window = None
|
||||
|
||||
def on_notification_count_changed(self, _service, count):
|
||||
"""Handle notification count changes."""
|
||||
print(f"[NotificationIndicator] Count changed to {count}")
|
||||
self.notification_count = count
|
||||
self.update_count()
|
||||
|
||||
# If popup is open, refresh it to show new notifications
|
||||
if self.popup_window and self.popup_window.get_visible():
|
||||
print(f"[NotificationIndicator] Popup is open, refreshing...")
|
||||
self.popup_window.refresh_popup()
|
||||
|
||||
def on_notification_added(self, _service, _notification_id):
|
||||
"""Handle new notifications being added."""
|
||||
print(f"[NotificationIndicator] New notification added: {_notification_id}")
|
||||
# Update count immediately when new notification arrives
|
||||
self.notification_count = self.notification_service.count
|
||||
self.update_count()
|
||||
|
||||
# If popup is open, refresh it to show new notifications
|
||||
if self.popup_window and self.popup_window.get_visible():
|
||||
print(f"[NotificationIndicator] Popup is open, refreshing...")
|
||||
self.popup_window.refresh_popup()
|
||||
|
||||
def on_dnd_changed(self, _service, dnd_enabled):
|
||||
"""Handle DND status changes."""
|
||||
print(f"[NotificationIndicator] DND changed: {dnd_enabled}")
|
||||
self.update_count() # This will update the icon based on DND status
|
||||
|
||||
# If popup is open, refresh it to reflect DND state
|
||||
if self.popup_window and self.popup_window.get_visible():
|
||||
print(f"[NotificationIndicator] DND changed, refreshing popup...")
|
||||
self.popup_window.refresh_popup()
|
||||
|
||||
def update_count(self):
|
||||
"""Update the notification icon display."""
|
||||
count = self.notification_service.count
|
||||
self.notification_count = count
|
||||
dnd_enabled = self.notification_service.dont_disturb
|
||||
|
||||
if dnd_enabled:
|
||||
# Show DND icon when DND is enabled
|
||||
self.notification_icon.set_markup(icons.notifications_off)
|
||||
self.set_tooltip_text(f"Do Not Disturb enabled ({count} notification{'s' if count != 1 else ''})")
|
||||
elif count >= 1:
|
||||
# Show notifications_dots icon when there are notifications
|
||||
self.notification_icon.set_markup(icons.notifications_dots)
|
||||
self.set_tooltip_text(f"{count} notification{'s' if count != 1 else ''}")
|
||||
else:
|
||||
# Show regular notifications icon when no notifications
|
||||
self.notification_icon.set_markup(icons.notifications)
|
||||
self.set_tooltip_text("No notifications")
|
||||
|
||||
def on_clicked(self, _button):
|
||||
"""Handle click to show/hide notification popup."""
|
||||
if self.popup_window and self.popup_window.get_visible():
|
||||
self.popup_window.set_visible(False)
|
||||
else:
|
||||
self.show_notifications_popup()
|
||||
|
||||
def show_notifications_popup(self):
|
||||
"""Show popup window with cached notifications."""
|
||||
if self.popup_window:
|
||||
self.popup_window.destroy()
|
||||
|
||||
# Get cached notifications (always get fresh data)
|
||||
notifications = self.notification_service.get_deserialized()
|
||||
print(f"[NotificationIndicator] Opening popup with {len(notifications)} notifications")
|
||||
|
||||
# Create popup window (handles both empty and filled states)
|
||||
self.popup_window = NotificationPopupWindow(notifications, self.notification_service)
|
||||
self.popup_window.show_all()
|
||||
|
||||
|
||||
class NotificationPopupWindow(Window):
|
||||
def __init__(self, notifications, notification_service):
|
||||
self.notification_service = notification_service
|
||||
|
||||
# Create scrolled container for notifications
|
||||
self.notifications_box = Box(
|
||||
name="notifications-popup-box",
|
||||
orientation="v",
|
||||
spacing=8,
|
||||
h_expand=True,
|
||||
v_expand=False
|
||||
)
|
||||
|
||||
if not notifications:
|
||||
# Show "No notifications" message
|
||||
no_notif_icon = Label(
|
||||
name="no-notifications-icon",
|
||||
markup=icons.notifications,
|
||||
h_align="center"
|
||||
)
|
||||
|
||||
no_notif_message = Label(
|
||||
name="no-notifications-message",
|
||||
label="No notifications",
|
||||
h_align="center",
|
||||
v_align="center"
|
||||
)
|
||||
|
||||
no_notif_content = Box(
|
||||
name="no-notifications-content",
|
||||
orientation="v",
|
||||
spacing=16,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[no_notif_icon, no_notif_message]
|
||||
)
|
||||
|
||||
self.notifications_box.add(no_notif_content)
|
||||
else:
|
||||
# Add notifications to the box (limit to recent 10)
|
||||
recent_notifications = notifications[-10:] if len(notifications) > 10 else notifications
|
||||
recent_notifications.reverse() # Show newest first
|
||||
|
||||
for notification in recent_notifications:
|
||||
# Create notification widget with dock callback passed during initialization
|
||||
notif_widget = NotificationWidget(
|
||||
notification,
|
||||
show_progress=False,
|
||||
dock_callback=self.remove_notification_from_cache
|
||||
)
|
||||
|
||||
self.notifications_box.add(notif_widget)
|
||||
|
||||
# Create scrolled window with proper sizing
|
||||
scrolled = ScrolledWindow(
|
||||
name="notifications-scrolled",
|
||||
child=self.notifications_box,
|
||||
h_scrollbar_policy="never",
|
||||
v_scrollbar_policy="automatic",
|
||||
min_content_size=(400, 450),
|
||||
max_content_size=(450, 500),
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
propagate_width=False,
|
||||
propagate_height=False
|
||||
)
|
||||
|
||||
# Create header with DND toggle and clear all button
|
||||
header_children = [
|
||||
Label(
|
||||
name="notifications-title",
|
||||
label="Notifications",
|
||||
h_align="start",
|
||||
h_expand=True
|
||||
)
|
||||
]
|
||||
|
||||
# Add DND toggle button with icon
|
||||
dnd_icon = icons.notifications_off if self.notification_service.dont_disturb else icons.notifications
|
||||
dnd_tooltip = "Do Not Disturb: ON" if self.notification_service.dont_disturb else "Do Not Disturb: OFF"
|
||||
self.dnd_button = Button(
|
||||
name="dnd-toggle-button",
|
||||
child=Label(
|
||||
name="dnd-toggle-icon",
|
||||
markup=dnd_icon
|
||||
),
|
||||
on_clicked=self.toggle_dnd
|
||||
)
|
||||
self.dnd_button.set_tooltip_text(dnd_tooltip)
|
||||
header_children.append(self.dnd_button)
|
||||
|
||||
if notifications: # Only add clear button if there are notifications
|
||||
header_children.append(
|
||||
Button(
|
||||
name="clear-all-button",
|
||||
label="Clear All",
|
||||
on_clicked=self.clear_all_notifications
|
||||
)
|
||||
)
|
||||
|
||||
header_box = Box(
|
||||
name="notifications-header",
|
||||
orientation="h",
|
||||
spacing=8,
|
||||
children=header_children
|
||||
)
|
||||
|
||||
main_box = Box(
|
||||
name="notifications-popup-main",
|
||||
orientation="v",
|
||||
spacing=8,
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[header_box, scrolled]
|
||||
)
|
||||
|
||||
# Determine popup position based on dock position
|
||||
dock_position = data.DOCK_POSITION
|
||||
popup_anchor = self._get_popup_anchor(dock_position)
|
||||
|
||||
print(f"[NotificationPopup] Dock position: {dock_position}, Popup anchor: {popup_anchor}")
|
||||
|
||||
# Get margin based on dock position
|
||||
margin = self._get_popup_margin(dock_position)
|
||||
|
||||
super().__init__(
|
||||
name="notifications-popup-window",
|
||||
anchor=popup_anchor,
|
||||
margin=margin,
|
||||
child=main_box,
|
||||
layer="top",
|
||||
exclusive=False,
|
||||
keyboard_mode="on-demand",
|
||||
visible=True,
|
||||
all_visible=True
|
||||
)
|
||||
|
||||
# Auto-hide when clicking outside or pressing escape
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
|
||||
# Make sure the window can receive key events
|
||||
self.set_can_focus(True)
|
||||
self.grab_focus()
|
||||
|
||||
# Set tooltip with keyboard shortcuts
|
||||
self.set_tooltip_text("Keyboard shortcuts:\n• Escape: Close popup\n• Ctrl+D: Toggle DND\n• Ctrl+A: Clear all notifications")
|
||||
|
||||
def _get_popup_anchor(self, dock_position):
|
||||
"""Determine popup anchor position based on dock position."""
|
||||
# Position popup on the opposite side of the dock for better UX
|
||||
anchor_map = {
|
||||
"Top": "top", # Dock at top -> popup at top right
|
||||
"Bottom": "bottom", # Dock at bottom -> popup at bottom right
|
||||
"Left": "left", # Dock at left -> popup at top left
|
||||
"Right": "right" # Dock at right -> popup at top right
|
||||
}
|
||||
return anchor_map.get(dock_position, "bottom") # Default fallback
|
||||
|
||||
def _get_popup_margin(self, dock_position):
|
||||
"""Get appropriate margin based on dock position."""
|
||||
# Add margin to avoid overlapping with dock
|
||||
margin_map = {
|
||||
"Top": "60px 10px 10px 10px", # Top dock -> margin from top
|
||||
"Bottom": "10px 10px 60px 10px", # Bottom dock -> margin from bottom
|
||||
"Left": "10px 10px 10px 60px", # Left dock -> margin from left
|
||||
"Right": "10px 60px 10px 10px" # Right dock -> margin from right
|
||||
}
|
||||
return margin_map.get(dock_position, "10px 10px 10px 10px") # Default fallback
|
||||
|
||||
def update_dnd_button_icon(self):
|
||||
"""Update the DND button icon and tooltip to reflect current DND state."""
|
||||
if hasattr(self, 'dnd_button'):
|
||||
dnd_enabled = self.notification_service.dont_disturb
|
||||
dnd_icon = icons.notifications_off if dnd_enabled else icons.notifications
|
||||
dnd_tooltip = "Do Not Disturb: ON" if dnd_enabled else "Do Not Disturb: OFF"
|
||||
|
||||
# Update the button icon
|
||||
button_label = self.dnd_button.get_child()
|
||||
button_label.set_markup(dnd_icon)
|
||||
|
||||
# Update tooltip
|
||||
self.dnd_button.set_tooltip_text(dnd_tooltip)
|
||||
|
||||
print(f"[NotificationPopup] Updated DND button icon: {dnd_icon}")
|
||||
|
||||
|
||||
|
||||
|
||||
def remove_notification_from_cache(self, notification):
|
||||
"""Remove a specific notification from cache and refresh popup."""
|
||||
print(f"[NotificationPopup] Removing notification from cache: {notification.summary}")
|
||||
|
||||
# Find and remove the notification from the service cache
|
||||
# We need to find it by matching summary and app_name since IDs might differ
|
||||
all_notifications = self.notification_service.all_notifications
|
||||
for i, cached_notif in enumerate(all_notifications):
|
||||
if (cached_notif.get("summary") == notification.summary and
|
||||
cached_notif.get("app_name") == notification.app_name):
|
||||
print(f"[NotificationPopup] Found matching notification, removing...")
|
||||
self.notification_service.all_notifications.pop(i)
|
||||
self.notification_service._write_notifications(self.notification_service.all_notifications)
|
||||
self.notification_service.emit("notification_count", len(self.notification_service.all_notifications))
|
||||
break
|
||||
|
||||
# Refresh the popup immediately
|
||||
self.refresh_popup()
|
||||
|
||||
def on_notification_closed(self, notification, reason):
|
||||
"""Handle when a notification is closed - refresh the popup."""
|
||||
# Small delay to allow the notification service to update
|
||||
from gi.repository import GLib
|
||||
GLib.timeout_add(100, self.refresh_popup)
|
||||
|
||||
def refresh_popup(self):
|
||||
"""Refresh the popup content."""
|
||||
# Get updated notifications
|
||||
notifications = self.notification_service.get_deserialized()
|
||||
|
||||
# Clear current content
|
||||
for child in self.notifications_box.get_children():
|
||||
self.notifications_box.remove(child)
|
||||
|
||||
if not notifications:
|
||||
# Show "No notifications" message
|
||||
no_notif_icon = Label(
|
||||
name="no-notifications-icon",
|
||||
markup=icons.notifications,
|
||||
h_align="center"
|
||||
)
|
||||
|
||||
no_notif_message = Label(
|
||||
name="no-notifications-message",
|
||||
label="No notifications",
|
||||
h_align="center",
|
||||
v_align="center"
|
||||
)
|
||||
|
||||
no_notif_content = Box(
|
||||
name="no-notifications-content",
|
||||
orientation="v",
|
||||
spacing=16,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[no_notif_icon, no_notif_message]
|
||||
)
|
||||
|
||||
self.notifications_box.add(no_notif_content)
|
||||
else:
|
||||
# Add updated notifications
|
||||
recent_notifications = notifications[-10:] if len(notifications) > 10 else notifications
|
||||
recent_notifications.reverse() # Show newest first
|
||||
|
||||
for notification in recent_notifications:
|
||||
notif_widget = NotificationWidget(
|
||||
notification,
|
||||
show_progress=False,
|
||||
dock_callback=self.remove_notification_from_cache
|
||||
)
|
||||
self.notifications_box.add(notif_widget)
|
||||
|
||||
self.notifications_box.show_all()
|
||||
return False # Don't repeat the timeout
|
||||
|
||||
def toggle_dnd(self, button):
|
||||
"""Toggle Do Not Disturb mode."""
|
||||
current_dnd = self.notification_service.dont_disturb
|
||||
new_dnd = not current_dnd
|
||||
self.notification_service.dont_disturb = new_dnd
|
||||
|
||||
print(f"[NotificationPopup] DND toggled: {new_dnd}")
|
||||
|
||||
# Update the DND button icon using the centralized method
|
||||
self.update_dnd_button_icon()
|
||||
|
||||
# Emit DND signal
|
||||
self.notification_service.emit("dnd", new_dnd)
|
||||
|
||||
def clear_all_notifications(self, _button):
|
||||
"""Clear all cached notifications."""
|
||||
self.notification_service.clear_all_notifications()
|
||||
self.set_visible(False)
|
||||
|
||||
def on_key_press(self, _widget, event):
|
||||
"""Handle key press events."""
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Check if Escape key was pressed
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
print(f"[NotificationPopup] Escape key pressed, closing popup")
|
||||
self.set_visible(False)
|
||||
return True # Event handled
|
||||
|
||||
# Check for Ctrl+D to toggle DND
|
||||
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_d:
|
||||
print(f"[NotificationPopup] Ctrl+D pressed, toggling DND")
|
||||
# Toggle DND state directly
|
||||
current_dnd = self.notification_service.dont_disturb
|
||||
new_dnd = not current_dnd
|
||||
self.notification_service.dont_disturb = new_dnd
|
||||
|
||||
print(f"[NotificationPopup] DND toggled via keyboard: {new_dnd}")
|
||||
|
||||
# Update the DND button icon immediately
|
||||
self.update_dnd_button_icon()
|
||||
|
||||
# Emit DND signal
|
||||
self.notification_service.emit("dnd", new_dnd)
|
||||
|
||||
return True # Event handled
|
||||
|
||||
# Check for Ctrl+A to clear all notifications
|
||||
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_a:
|
||||
print(f"[NotificationPopup] Ctrl+A pressed, clearing all notifications")
|
||||
if self.notification_service.count > 0:
|
||||
self.clear_all_notifications(None) # Pass None as button parameter
|
||||
return True # Event handled
|
||||
|
||||
return False # Let other handlers process the event
|
||||
|
||||
def on_button_press(self, _widget, _event):
|
||||
"""Hide popup when clicking outside."""
|
||||
# This is a simple implementation - in a real scenario you'd want
|
||||
# more sophisticated outside-click detection
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class Notifications(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="notifications",
|
||||
orientation="h" if not data.VERTICAL else "v",
|
||||
spacing=4,
|
||||
children=[NotificationIndicator()],
|
||||
**kwargs
|
||||
)
|
||||
self.show_all()
|
||||
+115
-49
@@ -50,13 +50,44 @@ class ActionButton(Button):
|
||||
)
|
||||
|
||||
def on_clicked(self, *_):
|
||||
self.action.invoke()
|
||||
self.action.parent.close("dismissed-by-user")
|
||||
print(f"[ActionButton] Action button clicked: {self.action.label}")
|
||||
print(f"[ActionButton] Action details - Key: {getattr(self.action, 'key', 'N/A')}")
|
||||
print(f"[ActionButton] Action parent: {self.action.parent.app_name} - {self.action.parent.summary}")
|
||||
|
||||
# First, execute the action
|
||||
try:
|
||||
print(f"[ActionButton] Invoking action: {self.action.label}")
|
||||
result = self.action.invoke()
|
||||
print(f"[ActionButton] Action invoked successfully, result: {result}")
|
||||
print(f"[ActionButton] Action type: {type(self.action)}")
|
||||
|
||||
# Try to get more info about what the action does
|
||||
if hasattr(self.action, '__dict__'):
|
||||
print(f"[ActionButton] Action attributes: {self.action.__dict__}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ActionButton] Error invoking action: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Then, after a longer delay, handle notification removal
|
||||
if hasattr(self, '_dock_popup_callback') and self._dock_popup_callback is not None:
|
||||
print(f"[ActionButton] Scheduling dock notification removal in 500ms")
|
||||
# Longer delay to allow action to complete properly
|
||||
from gi.repository import GLib
|
||||
GLib.timeout_add(500, lambda: self._dock_popup_callback(self.action.parent))
|
||||
else:
|
||||
# For regular popup notifications, also add a small delay
|
||||
print(f"[ActionButton] Scheduling regular close in 200ms (no dock callback)")
|
||||
from gi.repository import GLib
|
||||
GLib.timeout_add(200, lambda: self.action.parent.close("dismissed-by-user"))
|
||||
|
||||
|
||||
class NotificationWidget(Box):
|
||||
def __init__(self, notification: Notification, timeout_ms=5000, **kwargs):
|
||||
def __init__(self, notification: Notification, timeout_ms=5000, show_progress=True, dock_callback=None, **kwargs):
|
||||
self.notification = notification
|
||||
self.show_progress = show_progress
|
||||
self._dock_popup_callback = dock_callback
|
||||
|
||||
self.is_expanded = False
|
||||
|
||||
@@ -81,30 +112,35 @@ class NotificationWidget(Box):
|
||||
self.timeout_ms = timeout_ms
|
||||
self._timeout_id = None
|
||||
|
||||
self.close_button_animator = Animator(
|
||||
bezier_curve=(0.25, 0.1, 0.25, 1.0),
|
||||
duration=0.3,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
)
|
||||
self.close_button_animator.connect(
|
||||
"notify::value", self.on_hover_animation_value_changed
|
||||
)
|
||||
if self.show_progress:
|
||||
self.close_button_animator = Animator(
|
||||
bezier_curve=(0.25, 0.1, 0.25, 1.0),
|
||||
duration=0.3,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
)
|
||||
self.close_button_animator.connect(
|
||||
"notify::value", self.on_hover_animation_value_changed
|
||||
)
|
||||
|
||||
self.timeout_progress_animator = Animator(
|
||||
bezier_curve=(1.0, 0.0, 1.0, 1.0),
|
||||
duration=timeout_ms / 1000.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
)
|
||||
self.timeout_progress_animator.connect(
|
||||
"notify::value", self.on_timeout_progress_changed
|
||||
)
|
||||
self.timeout_progress_animator.connect("finished", self.on_timeout_finished)
|
||||
self.timeout_progress_animator = Animator(
|
||||
bezier_curve=(1.0, 0.0, 1.0, 1.0),
|
||||
duration=timeout_ms / 1000.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
)
|
||||
self.timeout_progress_animator.connect(
|
||||
"notify::value", self.on_timeout_progress_changed
|
||||
)
|
||||
self.timeout_progress_animator.connect("finished", self.on_timeout_finished)
|
||||
|
||||
self.start_timeout()
|
||||
self.start_timeout()
|
||||
else:
|
||||
# No timeout for dock notifications
|
||||
self.close_button_animator = None
|
||||
self.timeout_progress_animator = None
|
||||
|
||||
def create_header(self, notification):
|
||||
app_icon = (
|
||||
@@ -216,35 +252,51 @@ class NotificationWidget(Box):
|
||||
def create_action_buttons(self, notification):
|
||||
if not notification.actions:
|
||||
return Box()
|
||||
|
||||
action_buttons = []
|
||||
for i, action in enumerate(notification.actions):
|
||||
action_button = ActionButton(action, i, len(notification.actions), self)
|
||||
# Pass the dock callback to action buttons if available
|
||||
if hasattr(self, '_dock_popup_callback') and self._dock_popup_callback is not None:
|
||||
print(f"[NotificationWidget] Setting dock callback on action button: {action.label}")
|
||||
action_button._dock_popup_callback = self._dock_popup_callback
|
||||
else:
|
||||
print(f"[NotificationWidget] No dock callback available for action button: {action.label}")
|
||||
action_buttons.append(action_button)
|
||||
|
||||
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)
|
||||
],
|
||||
children=action_buttons,
|
||||
)
|
||||
|
||||
def create_close_button(self):
|
||||
close_icon = Label(name="notif-close-label", markup=icons.cancel)
|
||||
|
||||
self.close_progress_bar = CircularProgressBar(
|
||||
name="notif-close-progress",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=0,
|
||||
end_angle=360,
|
||||
value=0.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
child=close_icon,
|
||||
)
|
||||
if self.show_progress:
|
||||
# Create with circular progress bar for timeout notifications
|
||||
self.close_progress_bar = CircularProgressBar(
|
||||
name="notif-close-progress",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=0,
|
||||
end_angle=360,
|
||||
value=0.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
child=close_icon,
|
||||
)
|
||||
button_child = self.close_progress_bar
|
||||
else:
|
||||
# Create simple button without progress bar for dock notifications
|
||||
self.close_progress_bar = None
|
||||
button_child = close_icon
|
||||
|
||||
close_button = Button(
|
||||
name="notif-close-button",
|
||||
child=self.close_progress_bar,
|
||||
on_clicked=lambda *_: self.notification.close("dismissed-by-user"),
|
||||
child=button_child,
|
||||
on_clicked=self.on_close_clicked,
|
||||
)
|
||||
close_button.connect(
|
||||
"enter-notify-event", lambda *_: self.hover_button(close_button)
|
||||
@@ -267,6 +319,17 @@ class NotificationWidget(Box):
|
||||
)
|
||||
return self.body_label
|
||||
|
||||
def on_close_clicked(self, *_):
|
||||
"""Handle close button click."""
|
||||
print(f"[NotificationWidget] Close button clicked for notification: {self.notification.summary}")
|
||||
|
||||
# For dock notifications, we need to manually remove from cache
|
||||
if hasattr(self, '_dock_popup_callback'):
|
||||
self._dock_popup_callback(self.notification)
|
||||
else:
|
||||
# For regular popup notifications
|
||||
self.notification.close("dismissed-by-user")
|
||||
|
||||
def create_chevron_button(self):
|
||||
# Only show chevron button if there's something to expand
|
||||
# (either long text that gets ellipsized or action buttons)
|
||||
@@ -306,6 +369,7 @@ class NotificationWidget(Box):
|
||||
return Box()
|
||||
|
||||
def toggle_expanded(self, *_):
|
||||
print(f"[NotificationWidget] Chevron button clicked, expanding: {not self.is_expanded}")
|
||||
self.is_expanded = not self.is_expanded
|
||||
self.expanded_revealer.set_reveal_child(self.is_expanded)
|
||||
|
||||
@@ -336,7 +400,7 @@ class NotificationWidget(Box):
|
||||
pass
|
||||
|
||||
def on_timeout_progress_changed(self, animator, value):
|
||||
if hasattr(self, "close_progress_bar"):
|
||||
if hasattr(self, "close_progress_bar") and self.close_progress_bar is not None:
|
||||
# Update progress bar value based on timeout progress (convert to float to avoid type issues)
|
||||
self.close_progress_bar.value = float(animator.value)
|
||||
|
||||
@@ -384,21 +448,23 @@ class NotificationWidget(Box):
|
||||
window.set_cursor(cursor)
|
||||
|
||||
def hover_button(self, button):
|
||||
self.pause_timeout()
|
||||
if self.show_progress:
|
||||
self.pause_timeout()
|
||||
self.set_pointer_cursor(button, "hand2")
|
||||
|
||||
# On hover, show full progress (indicating ready to close)
|
||||
if hasattr(self, "close_progress_bar"):
|
||||
if hasattr(self, "close_progress_bar") and self.close_progress_bar is not None:
|
||||
self.close_progress_bar.value = 1.0
|
||||
|
||||
def unhover_button(self, button):
|
||||
self.resume_timeout()
|
||||
if self.show_progress:
|
||||
self.resume_timeout()
|
||||
self.set_pointer_cursor(button, "arrow")
|
||||
|
||||
# On unhover, resume showing timeout progress
|
||||
if hasattr(self, "timeout_progress_animator") and hasattr(
|
||||
self, "close_progress_bar"
|
||||
):
|
||||
if (hasattr(self, "timeout_progress_animator") and
|
||||
hasattr(self, "close_progress_bar") and
|
||||
self.close_progress_bar is not None):
|
||||
# Resume the timeout progress display
|
||||
current_progress = float(self.timeout_progress_animator.value)
|
||||
self.close_progress_bar.value = current_progress
|
||||
|
||||
@@ -301,3 +301,60 @@ menu > menuitem:hover > label {
|
||||
#music-player-indicator-button:active #music-player-indicator-active {
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
/* Vertical mode specific styles */
|
||||
#music-player-container.vertical {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
#music-player-centerbox.vertical {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
#music-player-centerbox.vertical #music-track {
|
||||
font-size: 10px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
#music-player.vertical {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
/* Adjust controls layout for vertical mode */
|
||||
#music-controls.vertical {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#music-controls.vertical #music-prev,
|
||||
#music-controls.vertical #music-next,
|
||||
#music-controls.vertical #music-play-pause {
|
||||
padding: 2px;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
#music-controls.vertical #music-play-pause {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#music-controls.vertical label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#music-controls.vertical #music-play-pause label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Player indicators in vertical mode */
|
||||
#music-player-indicators.vertical {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#music-player-indicators.vertical #music-player-indicator-button {
|
||||
padding: 1px;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
#music-player-indicators.vertical #music-player-indicator,
|
||||
#music-player-indicators.vertical #music-player-indicator-active {
|
||||
font-size: 10px;
|
||||
}
|
||||
+169
-1
@@ -39,6 +39,174 @@
|
||||
|
||||
#network-label,
|
||||
#bluetooth-label,
|
||||
#recording-label {
|
||||
#recording-label,
|
||||
#notification-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#button-bar-notifications {
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#button-bar-notifications:hover {
|
||||
background-color: var(--surface-bright);
|
||||
}
|
||||
|
||||
|
||||
#notifications-popup-main {
|
||||
background-color: var(--surface);
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
#notifications-popup-box {
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
#notifications-scrolled {
|
||||
background-color: var(--surface);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#notifications-scrolled scrollbar {
|
||||
background-color: var(--surface-container);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#notifications-scrolled scrollbar slider {
|
||||
background-color: var(--outline);
|
||||
border-radius: 4px;
|
||||
min-width: 8px;
|
||||
min-height: 8px;
|
||||
}
|
||||
|
||||
#notifications-scrolled scrollbar slider:hover {
|
||||
background-color: var(--outline-variant);
|
||||
}
|
||||
|
||||
/* Style close button in dock notifications */
|
||||
#notifications-popup-window #notif-close-button {
|
||||
background-color: var(--surface-container);
|
||||
border-radius: 50%;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#notifications-popup-window #notif-close-button:hover {
|
||||
background-color: var(--error-container);
|
||||
}
|
||||
|
||||
#notifications-popup-window #notif-close-label {
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#notifications-popup-window #notif-close-button:hover #notif-close-label {
|
||||
color: var(--on-error-container);
|
||||
}
|
||||
|
||||
#notifications-header {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--outline);
|
||||
}
|
||||
|
||||
#notifications-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
#dnd-toggle-button {
|
||||
background-color: var(--surface-container);
|
||||
color: var(--on-surface-variant);
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--outline);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#dnd-toggle-button:hover {
|
||||
background-color: var(--surface-bright);
|
||||
}
|
||||
|
||||
#clear-all-button {
|
||||
background-color: var(--primary);
|
||||
color: var(--on-primary);
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
#clear-all-button:hover {
|
||||
background-color: var(--primary-bright);
|
||||
}
|
||||
|
||||
#notifications-content {
|
||||
padding: 8px;
|
||||
background-color: var(--surface-container);
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
#no-notifications-content {
|
||||
background-color: var(--surface-container);
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
margin: 20px;
|
||||
min-width: 292px;
|
||||
}
|
||||
|
||||
#no-notifications-icon {
|
||||
font-size: 48px;
|
||||
color: var(--on-surface-variant);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#no-notifications-message {
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#simple-notification-box {
|
||||
background-color: var(--surface-bright);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
#simple-notification-box:hover {
|
||||
background-color: var(--surface-dim);
|
||||
}
|
||||
|
||||
#notification-remove-button {
|
||||
background-color: var(--error);
|
||||
color: var(--on-error);
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#notification-remove-button:hover {
|
||||
background-color: var(--error-bright);
|
||||
}
|
||||
|
||||
#no-notifications-content {
|
||||
padding: 48px 32px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
#no-notifications-icon {
|
||||
font-size: 48px;
|
||||
color: var(--outline);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#no-notifications-message {
|
||||
font-size: 16px;
|
||||
color: var(--on-surface);
|
||||
opacity: 0.7;
|
||||
}
|
||||
+41
-15
@@ -12,6 +12,8 @@ from fabric.widgets.overlay import Overlay
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
from loguru import logger
|
||||
|
||||
import config.data as data
|
||||
|
||||
|
||||
def get_bars(file_path):
|
||||
config = configparser.ConfigParser()
|
||||
@@ -205,25 +207,49 @@ class Spectrum:
|
||||
def redraw(self, widget, cr):
|
||||
"""Draw spectrum graph"""
|
||||
cr.set_source_rgba(*self.color)
|
||||
dx = 3
|
||||
|
||||
center_y = self.sizes.area.height / 2 # center vertical of the drawing area
|
||||
for i, value in enumerate(self.audio_sample):
|
||||
width = self.sizes.area.width / self.sizes.number - self.sizes.padding
|
||||
radius = width / 2
|
||||
height = max(self.sizes.bar.height * min(value, 1), self.sizes.zero) / 2
|
||||
if height == self.sizes.zero / 2 + 1:
|
||||
height *= 0.5
|
||||
if data.VERTICAL:
|
||||
# Vertical mode: draw bars horizontally (rotated 90 degrees)
|
||||
dy = 3
|
||||
center_x = self.sizes.area.width / 2 # center horizontal of the drawing area
|
||||
|
||||
height = min(height, self.max_height)
|
||||
for i, value in enumerate(self.audio_sample):
|
||||
height = self.sizes.area.height / self.sizes.number - self.sizes.padding
|
||||
radius = height / 2
|
||||
width = max(self.sizes.bar.width * min(value, 1), self.sizes.zero) / 2
|
||||
if width == self.sizes.zero / 2 + 1:
|
||||
width *= 0.5
|
||||
|
||||
# Draw rectangle and arcs for rounded ends
|
||||
cr.rectangle(dx, center_y - height, width, height * 2)
|
||||
cr.arc(dx + radius, center_y - height, radius, 0, 2 * pi)
|
||||
cr.arc(dx + radius, center_y + height, radius, 0, 2 * pi)
|
||||
width = min(width, self.max_height)
|
||||
|
||||
cr.close_path()
|
||||
dx += width + self.sizes.padding
|
||||
# Draw rectangle and arcs for rounded ends (horizontal bars)
|
||||
cr.rectangle(center_x - width, dy, width * 2, height)
|
||||
cr.arc(center_x - width, dy + radius, radius, 0, 2 * pi)
|
||||
cr.arc(center_x + width, dy + radius, radius, 0, 2 * pi)
|
||||
|
||||
cr.close_path()
|
||||
dy += height + self.sizes.padding
|
||||
else:
|
||||
# Horizontal mode: draw bars vertically (original behavior)
|
||||
dx = 3
|
||||
center_y = self.sizes.area.height / 2 # center vertical of the drawing area
|
||||
|
||||
for i, value in enumerate(self.audio_sample):
|
||||
width = self.sizes.area.width / self.sizes.number - self.sizes.padding
|
||||
radius = width / 2
|
||||
height = max(self.sizes.bar.height * min(value, 1), self.sizes.zero) / 2
|
||||
if height == self.sizes.zero / 2 + 1:
|
||||
height *= 0.5
|
||||
|
||||
height = min(height, self.max_height)
|
||||
|
||||
# Draw rectangle and arcs for rounded ends (vertical bars)
|
||||
cr.rectangle(dx, center_y - height, width, height * 2)
|
||||
cr.arc(dx + radius, center_y - height, radius, 0, 2 * pi)
|
||||
cr.arc(dx + radius, center_y + height, radius, 0, 2 * pi)
|
||||
|
||||
cr.close_path()
|
||||
dx += width + self.sizes.padding
|
||||
cr.fill()
|
||||
|
||||
def size_update(self, *args):
|
||||
|
||||
@@ -84,6 +84,7 @@ bluetooth: str = ""
|
||||
night: str = ""
|
||||
coffee: str = ""
|
||||
notifications: str = ""
|
||||
notifications_dots: str = ""
|
||||
|
||||
wifi_off: str = ""
|
||||
bluetooth_off: str = ""
|
||||
|
||||
Reference in new issue
Block a user