💅 style(launcher, notification_popup): update keyboard focus styling and add discharging battery icons

This commit is contained in:
sankalp committed 2025-06-21 14:51:00 +05:45
1 parent 131670ea02
commit 5f55fb2c61
12 files changed
+531 -91

No files matched your search

+6 -4
View File
@@ -22,15 +22,17 @@ bind = SUPER, D, exec, $fabricSend "launcher.show_launcher('app ')"
bind = SUPER, V, exec, $fabricSend "launcher.show_launcher('clip ')"
# Wallpapers
bind = SUPER, W, exec, $fabricSend "launcher.show_launcher('wall ')"
bind = SHIFT ALT, W, exec, $fabricSend "launcher.show_launcher('wall random', external=True)" # Random Wallpaper
# Random Wallpaper
bind = SHIFT ALT, W, exec, $fabricSend "launcher.show_launcher('wall random', external=True)"
# Emoji Picker
bind = SUPER, E, exec, $fabricSend 'notch.open_notch("emoji")'
bind = SUPER, E, exec, $fabricSend "launcher.show_launcher('emoji ')"
# Power Menu
bind = SUPER, ESCAPE, exec, $fabricSend "launcher.show_launcher('power ')"
bind = SUPER SHIFT, M, exec, $fabricSend "launcher.show_launcher('caffeine on', external=True)" # Toggle Caffeine
# Toggle Caffeine
bind = SUPER SHIFT, M, exec, $fabricSend "launcher.show_launcher('caffeine on', external=True)"
# Settings
bind = SUPER, I, exec, uwsm-app $(python /home/sankalp/Modus/config/config.py) # Settings
# Restart Modus
# Restart Modus
bind = SUPER CTRL ALT, B, exec, killall modus; uwsm-app $(GTK_DEBUG=interactive python /home/sankalp/.config/Modus/main.py) # Restart with inspector
# Wallpapers directory: /home/sankalp/Pictures/wallpapers
+23
View File
@@ -0,0 +1,23 @@
{
"newww": {
"secret": "K3D4IHGVKQ2GQAJDCT7OSL4IS6PRTLXU",
"issuer": "",
"algorithm": "SHA1",
"digits": 6,
"period": 30
},
"n": {
"secret": "K3D4IHGVKQ2GQAJDCT7OSL4IS6PRTLXU",
"issuer": "",
"algorithm": "SHA1",
"digits": 6,
"period": 30
},
"qq": {
"secret": "K3D4IHGVKQ2GQAJDCT7OSL4IS6PRTLXU",
"issuer": "",
"algorithm": "SHA1",
"digits": 6,
"period": 30
}
}
+1 -2
View File
@@ -273,8 +273,7 @@ bind = {bind_vars.get("prefix_wallpapers", "SUPER")}, {bind_vars.get("suffix_wal
# Random Wallpaper
bind = {bind_vars.get("prefix_randwall", "SUPER")}, {bind_vars.get("suffix_randwall", "W")}, exec, $fabricSend "launcher.show_launcher('wall random', external=True)"
# Emoji Picker
# Emoji Picker
bind = {bind_vars.get("prefix_emoji", "SUPER")}, {bind_vars.get("suffix_emoji", "E")}, exec, $fabricSend "launcher.show_launcher('wall ')"
bind = {bind_vars.get("prefix_emoji", "SUPER")}, {bind_vars.get("suffix_emoji", "E")}, exec, $fabricSend "launcher.show_launcher('emoji ')"
# Power Menu
bind = {bind_vars.get("prefix_power", "SUPER")}, {bind_vars.get("suffix_power", "ESCAPE")}, exec, $fabricSend "launcher.show_launcher('power ')"
# Toggle Caffeine
+4 -5
View File
@@ -394,14 +394,13 @@ class Applications(Box):
for child in children:
self.add(child)
# Only show children if there are any applications
# Note: Component-level visibility is handled by the parent DockComponents class
# Show/hide the entire component based on whether there are applications
if len(children) > 0:
self.show_all()
else:
# Hide the widget content but don't override component-level visibility
for child in self.get_children():
child.hide()
# Hide the entire component when there are no applications
# This allows the dock to properly detect when it's empty and auto-hide
self.hide()
return True
+35 -16
View File
@@ -217,10 +217,16 @@ class Battery(Box):
else:
self.level.set_visible(False)
# Remove any existing profile classes
# Remove any existing profile and discharging classes
self.circle.remove_style_class("power-saver")
self.circle.remove_style_class("performance")
self.circle.remove_style_class("balanced")
self.circle.remove_style_class("discharging")
self.circle.remove_style_class("discharging-low")
self.circle.remove_style_class("discharging-critical")
self.icon.remove_style_class("discharging")
self.icon.remove_style_class("discharging-low")
self.icon.remove_style_class("discharging-critical")
# Apply power profile styling and icons
if power_profile:
@@ -230,9 +236,35 @@ class Battery(Box):
elif power_profile == "performance":
self.icon.set_markup(icons.power_performance)
else: # balanced
self.icon.set_markup(icons.battery)
# In balanced mode, show discharging icons when discharging
if state == "DISCHARGING":
# Show different discharging icons and apply styling based on battery level
if percentage <= 15:
self.icon.set_markup(icons.bat_low)
self.circle.add_style_class("discharging-critical")
self.icon.add_style_class("discharging-critical")
elif percentage <= 30:
self.icon.set_markup(icons.alert)
self.circle.add_style_class("discharging-low")
self.icon.add_style_class("discharging-low")
else:
self.icon.set_markup(icons.bat_discharging)
self.circle.add_style_class("discharging")
self.icon.add_style_class("discharging")
elif state == "CHARGING":
self.icon.set_markup(icons.charging)
else:
self.icon.set_markup(icons.battery)
else:
self.icon.set_markup(icons.battery)
# No power profile - show basic battery state icons
if state == "FULLY_CHARGED":
self.icon.set_markup(icons.battery)
elif state == "CHARGING":
self.icon.set_markup(icons.charging)
elif state == "DISCHARGING":
self.icon.set_markup(icons.discharging)
else:
self.icon.set_markup(icons.battery)
# Apply alert styling if battery is low AND not charging
if percentage <= 15 and not charging:
@@ -242,18 +274,5 @@ class Battery(Box):
self.icon.remove_style_class("alert")
self.circle.remove_style_class("alert")
# Update icon based on battery state if not in power profile mode
if not power_profile:
if state == "FULLY_CHARGED":
self.icon.set_markup(icons.battery)
elif state == "CHARGING":
self.icon.set_markup(icons.charging)
elif percentage <= 15 and state == "DISCHARGING":
self.icon.set_markup(icons.alert)
elif state == "DISCHARGING":
self.icon.set_markup(icons.discharging)
else:
self.icon.set_markup(icons.battery)
self.set_visible(True)
return True
+285 -28
View File
@@ -54,6 +54,10 @@ class Launcher(Window):
# Trigger system
self.triggered_plugin = None # Currently active triggered plugin
# Focus management for header buttons
self.focus_mode = "search" # "search", "results", "header"
self.header_button_index = 0 # 0 = config, 1 = close
# Setup UI
main_box = Box(
name="launcher",
@@ -75,28 +79,37 @@ class Launcher(Window):
self.search_entry.connect("changed", self._on_search_changed)
self.search_entry.connect("activate", self._on_entry_activate)
# Create header buttons with references for keyboard navigation
self.config_button = Button(
name="config-button",
child=Label(name="config-label", markup=icons.config),
tooltip_text="Open Configuration (Ctrl+,)",
on_clicked=lambda *_: (
exec_shell_command_async(
f"python {get_relative_path('../../config/config.py')}"
),
self.close_launcher(),
),
)
self.close_button = Button(
name="close-button",
child=Label(name="close-label", markup=icons.cancel),
tooltip_text="Close Launcher (Escape)",
on_clicked=lambda *_: self.close_launcher(),
)
# Store header buttons for keyboard navigation
self.header_buttons = [self.config_button, self.close_button]
self.header_box = Box(
name="header_box",
spacing=10,
orientation="h",
children=[
Button(
name="config-button",
child=Label(name="config-label", markup=icons.config),
on_clicked=lambda *_: (
exec_shell_command_async(
f"python {get_relative_path('../../config/config.py')}"
),
self.close_launcher(),
),
),
self.config_button,
self.search_entry,
Button(
name="close-button",
child=Label(name="close-label", markup=icons.cancel),
tooltip_text="Exit",
on_clicked=lambda *_: self.close_launcher(),
),
self.close_button,
],
)
@@ -180,17 +193,25 @@ class Launcher(Window):
self.search_entry.set_text("")
self._clear_results()
self.search_entry.grab_focus()
# Reset focus mode to search
self.focus_mode = "search"
self.header_button_index = 0
# Position cursor at the end if there's text
# Remove focus styling from header buttons
self._clear_header_focus()
# Focus search entry without selecting text
if trigger_keyword:
# For trigger keywords, we want the cursor at the end
self.search_entry.grab_focus()
def position_cursor():
if hasattr(self.search_entry, "set_position"):
self.search_entry.set_position(-1) # Move caret to end
return False # Only run once
GLib.idle_add(position_cursor)
else:
# For normal opening, use our method that prevents text selection
self._focus_search_entry_without_selection()
self.visible = True
@@ -211,6 +232,11 @@ class Launcher(Window):
if getattr(self, "_initializing", True):
return
# Reset focus to search when user types
if self.focus_mode != "search":
self.focus_mode = "search"
self._clear_header_focus()
query = entry.get_text().strip()
self.query = query
@@ -735,21 +761,55 @@ class Launcher(Window):
# Let normal backspace behavior continue for other cases
return False
# Up/Down - navigate results
# Ctrl+, - Open configuration
if keyval == Gdk.KEY_comma and event.state & Gdk.ModifierType.CONTROL_MASK:
self.config_button.emit("clicked")
return True
# Up/Down - navigate results (alternative to Tab)
if keyval == Gdk.KEY_Up:
if self.results:
self.selected_index = (self.selected_index - 1) % len(self.results)
if self.focus_mode == "results" and self.results:
if self.selected_index > 0:
# Move to previous result
self.selected_index -= 1
self._update_selection()
else:
# At first result, go back to search entry
self.focus_mode = "search"
self._focus_search_entry_without_selection()
elif self.results:
# If not in results mode but have results, enter results mode at last item
self.focus_mode = "results"
self.selected_index = len(self.results) - 1
self._update_selection()
return True
if keyval == Gdk.KEY_Down:
if self.results:
self.selected_index = (self.selected_index + 1) % len(self.results)
if self.focus_mode == "results" and self.results:
if self.selected_index < len(self.results) - 1:
# Move to next result
self.selected_index += 1
self._update_selection()
else:
# At last result, go to header buttons
self.focus_mode = "header"
self.header_button_index = 0
self._update_header_focus()
elif self.results:
# If not in results mode but have results, enter results mode at first item
self.focus_mode = "results"
self.selected_index = 0
self._update_selection()
return True
# Enter - activate selected result
# Enter - activate selected result or header button
if keyval == Gdk.KEY_Return:
# Check if we're in header mode
if self.focus_mode == "header":
# Activate the selected header button
self.header_buttons[self.header_button_index].emit("clicked")
return True
# Check for Shift+Enter to pin application
if event.state & Gdk.ModifierType.SHIFT_MASK:
if self.results and 0 <= self.selected_index < len(self.results):
@@ -775,13 +835,64 @@ class Launcher(Window):
self._activate_selected()
return True
# Tab - cycle through results
# Tab - cycle through focus areas, results, and header buttons
if keyval == Gdk.KEY_Tab:
if event.state & Gdk.ModifierType.SHIFT_MASK:
# Shift+Tab - reverse direction
if self.focus_mode == "header":
# Navigate between header buttons in reverse
self._navigate_header_buttons_backward()
elif self.focus_mode == "results":
# Navigate through results in reverse
self._navigate_results_backward()
else:
self._cycle_focus_backward()
else:
# Tab - forward direction
if self.focus_mode == "header":
# Navigate between header buttons forward
self._navigate_header_buttons_forward()
elif self.focus_mode == "results":
# Navigate through results forward
self._navigate_results_forward()
else:
self._cycle_focus_forward()
return True
# Page Up/Page Down - navigate results faster
if keyval == Gdk.KEY_Page_Up:
if self.results:
self.selected_index = (self.selected_index + 1) % len(self.results)
self.selected_index = max(0, self.selected_index - 5)
self._update_selection()
return True
if keyval == Gdk.KEY_Page_Down:
if self.results:
self.selected_index = min(len(self.results) - 1, self.selected_index + 5)
self._update_selection()
return True
# Home/End - go to first/last result
if keyval == Gdk.KEY_Home:
if self.results:
self.selected_index = 0
self._update_selection()
return True
if keyval == Gdk.KEY_End:
if self.results:
self.selected_index = len(self.results) - 1
self._update_selection()
return True
# Forward other keys to custom widgets if they can handle them
if self.results and 0 <= self.selected_index < len(self.results):
result = self.results[self.selected_index]
if result.custom_widget and hasattr(result.custom_widget, 'on_key_press'):
# Try to forward the key event to the custom widget
if result.custom_widget.on_key_press(result.custom_widget, event):
return True
return False
def _custom_widget_has_entry(self, widget):
@@ -863,6 +974,13 @@ class Launcher(Window):
# For custom widgets, we don't need to handle selection visually
# since they manage their own interaction
# Focus custom widgets when selected for keyboard interaction
if self.results and 0 <= self.selected_index < len(self.results):
result = self.results[self.selected_index]
if result.custom_widget and result.custom_widget.get_can_focus():
# Give focus to the custom widget for keyboard interaction
result.custom_widget.grab_focus()
# Scroll to make the selected item visible
if selected_widget and self.results_scroll.get_visible():
# Use immediate scrolling for better responsiveness
@@ -962,6 +1080,145 @@ class Launcher(Window):
return False # Don't repeat the idle callback
def _cycle_focus_forward(self):
"""Cycle focus forward: search -> results -> header (first button)"""
if self.focus_mode == "search":
if self.results:
self.focus_mode = "results"
self._update_selection()
else:
self.focus_mode = "header"
self.header_button_index = 0
self._update_header_focus()
elif self.focus_mode == "results":
self.focus_mode = "header"
self.header_button_index = 0
self._update_header_focus()
def _cycle_focus_backward(self):
"""Cycle focus backward: search -> header (last button) -> results -> search"""
if self.focus_mode == "search":
self.focus_mode = "header"
self.header_button_index = len(self.header_buttons) - 1
self._update_header_focus()
elif self.focus_mode == "results":
self.focus_mode = "search"
self._clear_header_focus()
self._focus_search_entry_without_selection()
def _update_header_focus(self):
"""Update focus to the selected header button."""
# Remove focus from all buttons first
self._clear_header_focus()
# Add focus style to selected button
selected_button = self.header_buttons[self.header_button_index]
selected_button.add_style_class("focused")
selected_button.grab_focus()
def _clear_header_focus(self):
"""Remove focus styling from all header buttons."""
for button in self.header_buttons:
button.remove_style_class("focused")
def _focus_search_entry_without_selection(self):
"""Focus search entry and position cursor at end without selecting text."""
# First grab focus
self.search_entry.grab_focus()
# Use multiple approaches to prevent text selection
def clear_selection():
try:
text_length = len(self.search_entry.get_text())
# Method 1: Set position and clear selection
if hasattr(self.search_entry, "set_position"):
self.search_entry.set_position(text_length)
if hasattr(self.search_entry, "select_region"):
self.search_entry.select_region(text_length, text_length)
# Method 2: Try to access underlying GTK widget
try:
# For fabric Entry widgets, try to get the actual GTK Entry
if hasattr(self.search_entry, '_entry'):
gtk_entry = self.search_entry._entry
elif hasattr(self.search_entry, 'get_children') and self.search_entry.get_children():
gtk_entry = self.search_entry.get_children()[0]
else:
gtk_entry = self.search_entry
if hasattr(gtk_entry, 'set_position'):
gtk_entry.set_position(text_length)
if hasattr(gtk_entry, 'select_region'):
gtk_entry.select_region(text_length, text_length)
except Exception:
pass
except Exception as e:
print(f"Could not clear selection: {e}")
return False
# Schedule clearing selection after focus is established
GLib.idle_add(clear_selection)
# Also try with a small delay as backup
GLib.timeout_add(10, clear_selection)
def _navigate_header_buttons_forward(self):
"""Navigate to next header button or exit header mode."""
if self.header_button_index < len(self.header_buttons) - 1:
# Move to next button
self.header_button_index += 1
self._update_header_focus()
else:
# Exit header mode and go to search
self.focus_mode = "search"
self._clear_header_focus()
self._focus_search_entry_without_selection()
def _navigate_header_buttons_backward(self):
"""Navigate to previous header button or exit header mode."""
if self.header_button_index > 0:
# Move to previous button
self.header_button_index -= 1
self._update_header_focus()
else:
# Exit header mode and go to results or search
if self.results:
self.focus_mode = "results"
self.selected_index = len(self.results) - 1 # Go to last result
self._clear_header_focus()
self._update_selection()
else:
self.focus_mode = "search"
self._clear_header_focus()
self._focus_search_entry_without_selection()
def _navigate_results_forward(self):
"""Navigate to next result or exit results mode."""
if self.results and self.selected_index < len(self.results) - 1:
# Move to next result
self.selected_index += 1
self._update_selection()
else:
# Exit results mode and go to header
self.focus_mode = "header"
self.header_button_index = 0
self._update_header_focus()
def _navigate_results_backward(self):
"""Navigate to previous result or exit results mode."""
if self.results and self.selected_index > 0:
# Move to previous result
self.selected_index -= 1
self._update_selection()
else:
# Exit results mode and go to search
self.focus_mode = "search"
self._focus_search_entry_without_selection()
def _activate_selected(self):
"""Activate the currently selected result."""
if self.results and 0 <= self.selected_index < len(self.results):
+33 -1
View File
@@ -34,6 +34,10 @@ class CalendarWidget(Gtk.Box):
self.today = datetime.date.today()
self.plugin = plugin_instance
# Make widget focusable for keyboard events
self.set_can_focus(True)
self.connect("key-press-event", self._on_key_press)
self.setup_ui()
def setup_ui(self):
@@ -127,6 +131,33 @@ class CalendarWidget(Gtk.Box):
if self.plugin:
self.plugin._navigate_to_next()
def _on_key_press(self, widget, event):
"""Handle keyboard navigation for calendar."""
from gi.repository import Gdk
keyval = event.keyval
# Left arrow - previous month
if keyval == Gdk.KEY_Left:
if self.plugin:
self.plugin._navigate_to_previous()
return True
# Right arrow - next month
if keyval == Gdk.KEY_Right:
if self.plugin:
self.plugin._navigate_to_next()
return True
# Home - go to current month
if keyval == Gdk.KEY_Home:
if self.plugin:
self.plugin._reset_to_current_month()
self.plugin._force_launcher_refresh()
return True
return False
def update_display(self, month: int, year: int):
"""Update the calendar to show a different month/year."""
self.display_month = month
@@ -214,6 +245,7 @@ class CalendarPlugin(PluginBase):
def initialize(self):
"""Initialize the Calendar plugin."""
self.set_triggers(["cal", "cal "])
self.description = "Calendar with visual dates and navigation. Use ← → arrows for months, Home for current month"
def cleanup(self):
"""Cleanup the Calendar plugin."""
@@ -306,7 +338,7 @@ class CalendarPlugin(PluginBase):
results.append(
Result(
title=f"Calendar - {month_name} {display_year}",
subtitle="Use ◀ ▶ buttons to navigate months",
subtitle="← → navigate months, Home for current month",
icon_markup=icons.calendar,
action=lambda: None,
relevance=1.0,
+47 -2
View File
@@ -127,9 +127,13 @@ class KanbanNote(Gtk.EventBox):
super().__init__()
self.text = text
# Make focusable for keyboard events
self.set_can_focus(True)
self.setup_ui()
self.setup_dnd()
self.connect("button-press-event", self.on_button_press)
self.connect("key-press-event", self.on_key_press)
def setup_ui(self):
self.box = Gtk.Box(name="kanban-note", spacing=4)
@@ -167,6 +171,24 @@ class KanbanNote(Gtk.EventBox):
self.start_edit()
return False
def on_key_press(self, widget, event):
"""Handle keyboard events for kanban notes."""
from gi.repository import Gdk
keyval = event.keyval
# Enter or F2 - start editing
if keyval in (Gdk.KEY_Return, Gdk.KEY_F2):
self.start_edit()
return True
# Delete - remove note
if keyval == Gdk.KEY_Delete:
self.get_parent().destroy()
return True
return False
def on_drag_begin(self, widget, context):
surface = createSurfaceFromWidget(self)
Gtk.drag_set_icon_surface(context, surface)
@@ -405,12 +427,35 @@ class Kanban(Gtk.Box):
def on_key_press(self, widget, event):
"""Handle key press events for the kanban widget."""
if event.keyval == Gdk.KEY_Return:
from gi.repository import Gdk
keyval = event.keyval
# Enter - add pending text as note
if keyval == Gdk.KEY_Return:
if self.pending_add_text:
self.columns[0].add_note(self.pending_add_text)
self.save_state()
self.pending_add_text = None
return True
# Ctrl+N - add new note to first column
if keyval == Gdk.KEY_n and event.state & Gdk.ModifierType.CONTROL_MASK:
self.columns[0].on_add_clicked(None)
return True
# Ctrl+1, Ctrl+2, Ctrl+3 - add note to specific column
if event.state & Gdk.ModifierType.CONTROL_MASK:
if keyval == Gdk.KEY_1 and len(self.columns) > 0:
self.columns[0].on_add_clicked(None)
return True
elif keyval == Gdk.KEY_2 and len(self.columns) > 1:
self.columns[1].on_add_clicked(None)
return True
elif keyval == Gdk.KEY_3 and len(self.columns) > 2:
self.columns[2].on_add_clicked(None)
return True
return False
def on_hidden_entry_activate(self, entry):
@@ -471,7 +516,7 @@ class KanbanPlugin(PluginBase):
def initialize(self):
"""Initialize the Kanban plugin."""
self.set_triggers(["kanban", "kanban "])
self.description = "Kanban board for task management. Use 'kanban add <text>' to quickly add tasks."
self.description = "Kanban board for task management. Use 'kanban add <text>' to quickly add tasks. Keyboard: Enter/F2 to edit, Del to delete, Ctrl+N to add new note"
def cleanup(self):
"""Cleanup the Kanban plugin."""
+24 -2
View File
@@ -27,13 +27,17 @@ class NetworkPasswordEntry(Box):
self.access_point = access_point
self.network_plugin = network_plugin
# Make widget focusable for keyboard events
self.set_can_focus(True)
self.connect("key-press-event", self.on_key_press)
# Password entry field
self.password_entry = Entry(
name="network-password-field",
placeholder="Enter password",
h_expand=True,
h_align="fill",
visibility=False
visibility=False
)
# Connect Enter key to connect action
@@ -42,7 +46,6 @@ class NetworkPasswordEntry(Box):
# Auto-focus the password entry when widget is created
GLib.timeout_add(100, self._focus_password_entry)
# Add widgets to layout
self.add(self.password_entry)
@@ -54,6 +57,24 @@ class NetworkPasswordEntry(Box):
print(f"Could not focus password entry: {e}")
return False # Don't repeat
def on_key_press(self, widget, event):
"""Handle keyboard events for password entry."""
from gi.repository import Gdk
keyval = event.keyval
# Enter - connect to network
if keyval == Gdk.KEY_Return:
self.connect_to_network()
return True
# Escape - cancel password entry
if keyval == Gdk.KEY_Escape:
self.cancel_password_entry()
return True
return False
def connect_to_network(self):
"""Connect to network with entered password."""
password = self.password_entry.get_text()
@@ -105,6 +126,7 @@ class NetworkPlugin(PluginBase):
def initialize(self):
"""Initialize the network plugin."""
self.set_triggers(["net"])
self.description = "Manage WiFi networks and connections. Keyboard: Enter to connect, Esc to cancel password entry"
def cleanup(self):
"""Cleanup the network plugin."""
+33 -31
View File
@@ -80,27 +80,24 @@ 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,
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.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,
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("notify::value", self.on_timeout_progress_changed)
self.timeout_progress_animator.connect("finished", self.on_timeout_finished)
self.start_timeout()
@@ -226,7 +223,10 @@ class NotificationWidget(Box):
)
def create_close_button(self):
close_icon = Label(name="notif-close-label", markup=icons.cancel)
close_icon = Label(
name="notif-close-label",
markup=icons.cancel
)
self.close_progress_bar = CircularProgressBar(
name="notif-close-progress",
@@ -234,7 +234,7 @@ class NotificationWidget(Box):
line_width=2,
start_angle=0,
end_angle=360,
value=0.0,
value=0.0,
min_value=0.0,
max_value=1.0,
child=close_icon,
@@ -270,14 +270,17 @@ class NotificationWidget(Box):
# Only show chevron button if there's something to expand
# (either long text that gets ellipsized or action buttons)
has_expandable_content = (
(self.notification.body and len(self.notification.body) > 50) # Long text
or bool(self.notification.actions) # Has action buttons
(self.notification.body and len(self.notification.body) > 50) or # Long text
bool(self.notification.actions) # Has action buttons
)
if not has_expandable_content:
return Box() # Return empty box if nothing to expand
self.chevron_icon = Label(name="notif-chevron-label", markup=icons.chevron_down)
self.chevron_icon = Label(
name="notif-chevron-label",
markup=icons.chevron_down
)
chevron_button = Button(
name="notif-chevron-button",
@@ -299,7 +302,9 @@ class NotificationWidget(Box):
name="notification-expanded-content",
orientation="v",
spacing=8,
children=[self.create_action_buttons(notification)],
children=[
self.create_action_buttons(notification)
],
)
else:
return Box()
@@ -315,18 +320,17 @@ class NotificationWidget(Box):
self.chevron_icon.set_markup(icons.chevron_down)
# Toggle body label properties
if hasattr(self, "body_label") and self.body_label:
if hasattr(self, 'body_label') and self.body_label:
if self.is_expanded:
# Show full text without ellipsization and enable wrapping
# Keep original newlines
self.body_label.set_markup(self.notification.body)
self.body_label.set_property("ellipsize", 0)
self.body_label.set_markup(self.notification.body) # Keep original newlines
self.body_label.set_property("ellipsize", 0)
self.body_label.set_property("wrap", True)
self.body_label.set_property("wrap-mode", 2)
self.body_label.set_property("wrap-mode", 2)
else:
# Show truncated text with ellipsization
self.body_label.set_markup(self.notification.body.replace("\n", " "))
self.body_label.set_property("ellipsize", 3)
self.body_label.set_property("ellipsize", 3)
self.body_label.set_property("wrap", False)
def on_hover_animation_value_changed(self, animator, value):
@@ -335,7 +339,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'):
# Update progress bar value based on timeout progress (convert to float to avoid type issues)
self.close_progress_bar.value = float(animator.value)
@@ -345,7 +349,7 @@ class NotificationWidget(Box):
def start_timeout(self):
self.stop_timeout()
if hasattr(self, "timeout_progress_animator"):
if hasattr(self, 'timeout_progress_animator'):
self.timeout_progress_animator.play()
self._timeout_id = GLib.timeout_add(self.timeout_ms, self.close_notification)
@@ -353,7 +357,7 @@ class NotificationWidget(Box):
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id)
self._timeout_id = None
if hasattr(self, "timeout_progress_animator"):
if hasattr(self, 'timeout_progress_animator'):
self.timeout_progress_animator.stop()
def close_notification(self):
@@ -369,9 +373,9 @@ class NotificationWidget(Box):
def destroy(self):
self.stop_timeout()
if hasattr(self, "close_button_animator"):
if hasattr(self, 'close_button_animator'):
self.close_button_animator.stop()
if hasattr(self, "timeout_progress_animator"):
if hasattr(self, 'timeout_progress_animator'):
self.timeout_progress_animator.stop()
super().destroy()
@@ -387,7 +391,7 @@ class NotificationWidget(Box):
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'):
self.close_progress_bar.value = 1.0
def unhover_button(self, button):
@@ -395,9 +399,7 @@ class NotificationWidget(Box):
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'):
# Resume the timeout progress display
current_progress = float(self.timeout_progress_animator.value)
self.close_progress_bar.value = current_progress
+24
View File
@@ -64,6 +64,30 @@
border: 3px solid var(--red);
}
#battery-circle.discharging {
border: 3px solid var(--yellow);
}
#battery-circle.discharging-low {
border: 3px solid var(--orange);
}
#battery-circle.discharging-critical {
border: 3px solid var(--red-dim);
}
#battery-icon.discharging {
color: var(--yellow);
}
#battery-icon.discharging-low {
color: var(--orange);
}
#battery-icon.discharging-critical {
color: var(--red-dim);
}
/*
* ── metrics ───────────────────────────────────────────────────────────
*/
+16
View File
@@ -22,6 +22,22 @@
border-radius: 16px;
}
/* Keyboard focus styling for header buttons */
#close-button.focused,
#config-button.focused {
background-color: var(--primary);
border-radius: 16px;
box-shadow: 0 0 0 2px var(--primary-bright);
}
#close-button.focused #close-label {
color: var(--background);
}
#config-button.focused #config-label {
color: var(--background);
}
#close-button:active {
background-color: var(--red-dim);
border-radius: 40px;