💅 style(launcher, notification_popup): update keyboard focus styling and add discharging battery icons
This commit is contained in:
12 files changed
+531
-91
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user