From 5f55fb2c61397d04b0595938f0e7e2679f4c977f Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 21 Jun 2025 14:51:00 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85=20style(launcher,=20notification?= =?UTF-8?q?=5Fpopup):=20update=20keyboard=20focus=20styling=20and=20add=20?= =?UTF-8?q?discharging=20battery=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/hypr/modus.conf | 10 +- config/otp_accounts.json | 23 ++ config/settings/utils.py | 3 +- modules/dock/components/applications.py | 9 +- modules/dock/components/battery.py | 51 ++-- modules/launcher/main.py | 313 +++++++++++++++++++++--- modules/launcher/plugins/calendar.py | 34 ++- modules/launcher/plugins/kanban.py | 49 +++- modules/launcher/plugins/network.py | 26 +- modules/notification_popup.py | 64 ++--- styles/components.css | 24 ++ styles/launcher.css | 16 ++ 12 files changed, 531 insertions(+), 91 deletions(-) create mode 100644 config/otp_accounts.json diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index a63e071..cbccf5d 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -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 diff --git a/config/otp_accounts.json b/config/otp_accounts.json new file mode 100644 index 0000000..f28947f --- /dev/null +++ b/config/otp_accounts.json @@ -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 + } +} \ No newline at end of file diff --git a/config/settings/utils.py b/config/settings/utils.py index d3810d4..02ffcce 100644 --- a/config/settings/utils.py +++ b/config/settings/utils.py @@ -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 diff --git a/modules/dock/components/applications.py b/modules/dock/components/applications.py index 6cf2d7c..0fdc4f1 100644 --- a/modules/dock/components/applications.py +++ b/modules/dock/components/applications.py @@ -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 diff --git a/modules/dock/components/battery.py b/modules/dock/components/battery.py index 5abfa84..aa6a225 100644 --- a/modules/dock/components/battery.py +++ b/modules/dock/components/battery.py @@ -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 diff --git a/modules/launcher/main.py b/modules/launcher/main.py index 46eff5f..95c5929 100644 --- a/modules/launcher/main.py +++ b/modules/launcher/main.py @@ -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): diff --git a/modules/launcher/plugins/calendar.py b/modules/launcher/plugins/calendar.py index 2f256b1..8d35f26 100644 --- a/modules/launcher/plugins/calendar.py +++ b/modules/launcher/plugins/calendar.py @@ -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, diff --git a/modules/launcher/plugins/kanban.py b/modules/launcher/plugins/kanban.py index f752900..f4140d1 100644 --- a/modules/launcher/plugins/kanban.py +++ b/modules/launcher/plugins/kanban.py @@ -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 ' to quickly add tasks." + self.description = "Kanban board for task management. Use 'kanban add ' 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.""" diff --git a/modules/launcher/plugins/network.py b/modules/launcher/plugins/network.py index ed7cf0f..4b51888 100644 --- a/modules/launcher/plugins/network.py +++ b/modules/launcher/plugins/network.py @@ -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.""" diff --git a/modules/notification_popup.py b/modules/notification_popup.py index eb17904..df157cf 100644 --- a/modules/notification_popup.py +++ b/modules/notification_popup.py @@ -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 diff --git a/styles/components.css b/styles/components.css index ec5adcf..c1ae3b9 100644 --- a/styles/components.css +++ b/styles/components.css @@ -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 ─────────────────────────────────────────────────────────── */ diff --git a/styles/launcher.css b/styles/launcher.css index e6fe856..ee9f77e 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -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;