From 63f0b046ca432461cc24a0eb75d780dc146971ef Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 3 May 2026 22:06:48 +0545 Subject: [PATCH] =?UTF-8?q?feat(src/window):restructure=20dock=20UI=20and?= =?UTF-8?q?=20application=20switcher=20using=20config=E2=80=91driven=20lay?= =?UTF-8?q?out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/window/dock.py | 157 +++++++----- src/window/settings/main.py | 489 ++++++++++++++++++++++++++++++++++++ src/window/switcher.py | 91 ++++--- 3 files changed, 647 insertions(+), 90 deletions(-) create mode 100644 src/window/settings/main.py diff --git a/src/window/dock.py b/src/window/dock.py index 4b10c4e..fa117b4 100644 --- a/src/window/dock.py +++ b/src/window/dock.py @@ -20,7 +20,7 @@ from fabric.widgets.overlay import Overlay from fabric.widgets.revealer import Revealer from fabric.widgets.wayland import WaylandWindow as Window -import shared.data as data +from services.config import config, on_config_change from services.modus import modus_service from utils.functions import ( clear_children, @@ -44,15 +44,10 @@ class AppBar(Box): self.pinned_items_pos = [] self._parent = parent - # Set orientation based on dock position - orientation = ( - "vertical" if data.DOCK_POSITION in ["Left", "Right"] else "horizontal" - ) - super().__init__( spacing=0, name="dock", - orientation=orientation, + orientation="horizontal", children=[], ) self.icon_resolver = IconResolver() @@ -87,6 +82,39 @@ class AppBar(Box): self._app_monitor_timer_id = GLib.timeout_add(250, update_running_apps) GLib.idle_add(self.update_dock_apps) + def update_icon_size(self): + """Update all icons in the dock to the current config size""" + desktop_apps = get_desktop_applications(include_hidden=False) + + # Update pinned apps + for app_id, button in self.pinned_buttons.items(): + if hasattr(button, "icon_image"): + if app_id == "trash" or getattr(button, "is_trash", False): + icon_size = config().get("dock_icon_size", 52) + pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size) + button.icon_image.set_from_pixbuf(pixbuf) + else: + # Find app data to get icon + app_data = next( + ( + a + for a in self.pinned_apps + if self._matches_app_identifier(a, app_id) + ), + app_id, + ) + app = self._find_desktop_app(app_id, desktop_apps) + pixbuf = self._get_app_icon(app_data, app) + button.icon_image.set_from_pixbuf(pixbuf) + + # Update running apps + for addr, button in self.client_buttons.items(): + if hasattr(button, "icon_image"): + app_class = getattr(button, "app_class", "") + app = self._find_desktop_app(app_class, desktop_apps) + pixbuf = self._get_app_icon(app_class, app) + button.icon_image.set_from_pixbuf(pixbuf) + def destroy(self): """Clean up timers and children""" if hasattr(self, "_app_monitor_timer_id") and self._app_monitor_timer_id: @@ -131,10 +159,11 @@ class AppBar(Box): icon_pixbuf = self._get_app_icon(app_data, app) + icon_image = Image(name="dock_item_icon", pixbuf=icon_pixbuf) main_container = Box( name="dock_item_main_container", orientation="v", - children=[Image(name="dock_item_icon", pixbuf=icon_pixbuf)], + children=[icon_image], ) pinned_button = Button( @@ -153,14 +182,16 @@ class AppBar(Box): ) pinned_button.add_style_class("shown") + pinned_button.icon_image = icon_image self.pinned_buttons[app_identifier] = pinned_button self.pinned_apps_container.add(pinned_button) self.pinned_items_pos.append(pinned_button) def _get_app_icon(self, app_data, app=None): + icon_size = config().get("dock_icon_size", 52) if app: - return app.get_icon_pixbuf(data.DOCK_ICON_SIZE) + return app.get_icon_pixbuf(icon_size) icon_name = "" if isinstance(app_data, dict): @@ -169,19 +200,19 @@ class AppBar(Box): icon_name = app_data return self.icon_resolver.get_icon_pixbuf( - icon_name or "application-x-executable", data.DOCK_ICON_SIZE + icon_name or "application-x-executable", icon_size ) def _create_trash_button(self): """Create a trash button that opens the trash in file manager""" - trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf( - "user-trash", data.DOCK_ICON_SIZE - ) + icon_size = config().get("dock_icon_size", 52) + trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size) + trash_icon_image = Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf) main_container = Box( name="dock_item_main_container", orientation="v", - children=[Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf)], + children=[trash_icon_image], ) trash_button = Button( @@ -199,6 +230,7 @@ class AppBar(Box): trash_button.add_style_class("shown") trash_button.is_trash = True + trash_button.icon_image = trash_icon_image self.pinned_buttons["trash"] = trash_button self.pinned_apps_container.add(trash_button) @@ -646,6 +678,7 @@ class AppBar(Box): client_button.client_data = client client_button.app_class = app_class client_button.workspace_label = workspace_label + client_button.icon_image = client_image client_button.add_style_class("shown") self.client_buttons[instance_address] = client_button @@ -666,7 +699,7 @@ class AppBar(Box): return None def _should_show_app_instance(self, client): - if not data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS: + if not config().get("dock_hide_special_workspace_apps", True): return True workspace_id = self._get_workspace_id(client) @@ -755,19 +788,13 @@ class AppBar(Box): class Dock(Window): def __init__(self): - dock_config = self._get_dock_config() - if not data.DOCK_ENABLED: - super().__init__(layer="top", title="dock", anchor=dock_config["anchor"]) - self.children = Box() # Empty dock if disabled - return - - super().__init__(layer="top", anchor=dock_config["anchor"]) + super().__init__(layer="top", anchor="bottom center", title="modus-dock") self.app_bar = AppBar(self) self.revealer = Revealer( child=Box(children=[self.app_bar], style="padding: 20px 50px 5px 50px;"), transition_duration=200, - transition_type=dock_config["transition"], + transition_type="slide-up", ) self.children = EventBox( @@ -777,17 +804,51 @@ class Dock(Window): on_leave_notify_event=lambda *_: self.on_hover_leave(), ) - self.revealer.set_reveal_child(True) - self.app_bar.add_style_class("shown") + self._update_visibility() self.dock_height = 100 self.is_hovered = False self.hide_ticket = 0 - # Only setup occlusion monitoring if auto-hide is enabled - if data.DOCK_AUTO_HIDE: + # Subscribe to config changes for hot-reloading + on_config_change(self._on_config_change) + + if config().get("dock_auto_hide", True): self.setup_occlusion_monitoring() + def _on_config_change(self, new_config, old_config): + # Handle dock visibility + if config().has_changed("dock_enabled", old_config): + self._update_visibility() + + # Handle icon size changes + if config().has_changed("dock_icon_size", old_config): + self.app_bar.update_icon_size() + + # Handle auto-hide changes + if config().has_changed("dock_auto_hide", old_config): + if new_config.get("dock_auto_hide", True): + self.setup_occlusion_monitoring() + else: + if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id: + GLib.source_remove(self._occlusion_timer_id) + self._occlusion_timer_id = None + self.revealer.set_reveal_child(True) + self.app_bar.add_style_class("shown") + + # Handle special workspace apps visibility + if config().has_changed("dock_hide_special_workspace_apps", old_config): + self.app_bar.update_dock_apps() + + def _update_visibility(self): + enabled = config().get("dock_enabled", True) + if enabled: + self.show() + self.revealer.set_reveal_child(True) + self.app_bar.add_style_class("shown") + else: + self.hide() + def on_hover_enter(self): self.is_hovered = True self.hide_ticket = random.getrandbits(32) @@ -800,14 +861,10 @@ class Dock(Window): def delayed_hide(ticket): if ticket == self.hide_ticket and not self.is_hovered: - # Only hide if we aren't occluded (or if occlusion logic says so) - # For now, just trigger the basic hide if auto-hide is on - if data.DOCK_AUTO_HIDE: - # Check if actually occluded before hiding if DOCK_ALWAYS_OCCLUDED is false - occlusion_pos = self._get_dock_config()["occlusion"] - is_occluded = data.DOCK_ALWAYS_OCCLUDED or check_occlusion( - (occlusion_pos, self.dock_height) - ) + if config().get("dock_auto_hide", True): + is_occluded = config().get( + "dock_always_occluded", False + ) or check_occlusion(("bottom", self.dock_height)) if is_occluded: self.revealer.set_reveal_child(False) self.app_bar.remove_style_class("shown") @@ -815,34 +872,18 @@ class Dock(Window): GLib.timeout_add(500, delayed_hide, self.hide_ticket) - def _get_dock_config(self): - config_map = { - "Left": { - "anchor": "left center", - "transition": "slide-right", - "occlusion": "left", - }, - "Right": { - "anchor": "right center", - "transition": "slide-left", - "occlusion": "right", - }, - "Bottom": { - "anchor": "bottom center", - "transition": "slide-up", - "occlusion": "bottom", - }, - } - return config_map.get(data.DOCK_POSITION, config_map["Bottom"]) - def setup_occlusion_monitoring(self): + # Remove existing timer if any + if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id: + GLib.source_remove(self._occlusion_timer_id) + self._occlusion_timer_id = None + def check_dock_occlusion(): try: - if data.DOCK_ALWAYS_OCCLUDED: + if config().get("dock_always_occluded", False): is_occluded = True else: - occlusion_pos = self._get_dock_config()["occlusion"] - is_occluded = check_occlusion((occlusion_pos, self.dock_height)) + is_occluded = check_occlusion(("bottom", self.dock_height)) if ( is_occluded diff --git a/src/window/settings/main.py b/src/window/settings/main.py new file mode 100644 index 0000000..b3569dd --- /dev/null +++ b/src/window/settings/main.py @@ -0,0 +1,489 @@ +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.entry import Entry +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.stack import Stack +from fabric.utils import Gtk, GLib, Gdk + +from services.config import config, on_config_change +from utils.utils import setup_cursor_hover, svg_file + + +class SettingsRow(CenterBox): + def __init__(self, label: str, child=None, description: str = None, **kwargs): + label_widget = Label(label=label, name="settings-row-label", h_align="start") + + start_children = [label_widget] + if description: + description_widget = Label( + label=description, name="settings-row-description", h_align="start" + ) + start_children = [ + Box( + orientation="v", + spacing=2, + children=[label_widget, description_widget], + ) + ] + + super().__init__( + name="settings-row", + start_children=start_children, + end_children=[child] if child else [], + **kwargs, + ) + self.label_widget = label_widget + if description: + self.description_widget = description_widget + + +class SettingsSwitch(Button): + def __init__(self, config_key: str, **kwargs): + self.config_key = config_key + self.active = config().get(config_key, False) + + super().__init__(name="settings-switch", on_clicked=self.toggle, **kwargs) + self._update_style() + setup_cursor_hover(self, "pointer") + + # Sync with external config changes + on_config_change(self._on_config_change) + + def _on_config_change(self, new_config, old_config): + if config().has_changed(self.config_key, old_config): + self.active = new_config.get(self.config_key, False) + self._update_style() + + def toggle(self, *args): + self.active = not self.active + config().set(self.config_key, self.active) + config().save() + self._update_style() + + def _update_style(self): + if self.active: + self.add_style_class("active") + self.set_label("On") + else: + self.remove_style_class("active") + self.set_label("Off") + + +class SettingsEntry(Entry): + def __init__(self, config_key: str, **kwargs): + self.config_key = config_key + initial_value = str(config().get(config_key, "")) + + super().__init__( + name="settings-entry", + text=initial_value, + on_activate=self.on_change, + **kwargs, + ) + + # Sync with external config changes + on_config_change(self._on_config_change) + + def _on_config_change(self, new_config, old_config): + if config().has_changed(self.config_key, old_config): + new_val = str(new_config.get(self.config_key, "")) + if self.get_text() != new_val: + self.set_text(new_val) + + def on_change(self, entry, *args): + value = entry.get_text() + # Try to convert to int if possible + try: + if value.isdigit(): + value = int(value) + except Exception: + pass + + config().set(self.config_key, value) + config().save() + + +class SettingsComboBox(Gtk.ComboBoxText): + def __init__(self, config_key: str, options: list, **kwargs): + super().__init__(**kwargs) + self.set_name("settings-combo") + self.set_halign(Gtk.Align.END) + self.config_key = config_key + self.options = options + + for opt in options: + self.append_text(opt) + + self._update_from_config() + self.connect("changed", self.on_change) + + # Sync with external config changes + on_config_change(self._on_config_change) + + def _update_from_config(self): + current_val = str(config().get(self.config_key, "")) + if current_val in self.options: + self.set_active(self.options.index(current_val)) + + def _on_config_change(self, new_config, old_config): + if config().has_changed(self.config_key, old_config): + self._update_from_config() + + def on_change(self, combo): + value = combo.get_active_text() + if value: + config().set(self.config_key, value) + config().save() + + +class SettingsList(Box): + """A list of tags that can be added or removed""" + + def __init__(self, config_key: str, **kwargs): + super().__init__(name="settings-list", orientation="v", spacing=5, **kwargs) + self.config_key = config_key + + self.list_box = Box(spacing=5, name="settings-list-tags") + self.entry = Entry( + name="settings-list-entry", + placeholder="Add new item...", + on_activate=self.on_add, + ) + + self.add(self.list_box) + self.add(self.entry) + + self._update_list() + on_config_change(self._on_config_change) + + def _on_config_change(self, new_config, old_config): + if config().has_changed(self.config_key, old_config): + self._update_list() + + def _update_list(self): + items = config().get(self.config_key, []) + if not isinstance(items, list): + items = [] + + # Clear existing tags + self.list_box.children = [] + + for item in items: + tag = Button( + name="settings-list-tag", + child=Box( + spacing=5, + children=[ + Label(label=item), + Label(label="✕", name="settings-list-tag-close"), + ], + ), + on_clicked=lambda *_, i=item: self.on_remove(i), + ) + setup_cursor_hover(tag, "pointer") + self.list_box.pack_start(tag, False, False, 0) + + self.list_box.show_all() + + def on_add(self, entry): + val = entry.get_text().strip() + if not val: + return + + items = config().get(self.config_key, []) + if val not in items: + items.append(val) + config().set(self.config_key, items) + config().save() + + entry.set_text("") + self._update_list() + + def on_remove(self, item): + items = config().get(self.config_key, []) + if item in items: + items.remove(item) + config().set(self.config_key, items) + config().save() + self._update_list() + + +class SettingsPage(ScrolledWindow): + def __init__(self, title: str, rows: list, **kwargs): + container = Box( + name="settings-page-container", + orientation="v", + spacing=10, + h_expand=True, + v_expand=True, + children=[ + Label(label=title, name="settings-page-title", h_align="start"), + Box(orientation="v", spacing=1, children=rows, h_expand=True), + ], + ) + super().__init__( + name="settings-page", + h_expand=True, + v_expand=True, + propagate_width=True, + propagate_height=True, + min_content_width=600, + min_content_height=500, + **kwargs, + ) + self.add(container) + self.show_all() + self._container = container + + +class SettingsWindow(Gtk.Window): + def __init__(self, **kwargs): + GLib.set_prgname("modus-settings") + super().__init__(title="Modus Settings", **kwargs) + self.set_name("settings-window") + self.set_wmclass("modus-settings", "Modus") + self.set_default_size(850, 600) + self.set_resizable(False) + self.set_type_hint(Gtk.Window.get_type_hint(self) | Gdk.WindowTypeHint.DIALOG) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_visible(False) + + # Reset global singleton on destroy + self.connect("destroy", self._on_window_destroy) + + self.stack = Stack( + name="settings-stack", + transition_type="none", + h_expand=True, + v_expand=True, + ) + + self.pages = { + "general": self._create_general_page(), + "dock": self._create_dock_page(), + "panel": self._create_panel_page(), + "notifications": self._create_notifications_page(), + } + + for name, page in self.pages.items(): + self.stack.add_named(page, name) + + self.sidebar_buttons = {} + self.sidebar_buttons["general"] = self._create_sidebar_button( + "General", "general", "misc/logo.svg" + ) + self.sidebar_buttons["dock"] = self._create_sidebar_button( + "Dock", "dock", "misc/control.svg" + ) + self.sidebar_buttons["panel"] = self._create_sidebar_button( + "Panel", "panel", "misc/control-center.svg" + ) + self.sidebar_buttons["notifications"] = self._create_sidebar_button( + "Notifications", "notifications", "notifications/notification-active.svg" + ) + + self.set_page("general") + + self.sidebar = Box( + name="settings-sidebar", + orientation="v", + spacing=5, + children=list(self.sidebar_buttons.values()), + ) + + self.main_box = Box( + name="settings-main-box", + orientation="h", + h_expand=True, + v_expand=True, + children=[ + self.sidebar, + Box( + name="settings-content-wrapper", + children=[self.stack], + h_expand=True, + v_expand=True, + ), + ], + ) + + self.add(self.main_box) + + def _create_sidebar_button(self, label, page_name, icon_name): + btn = Button( + name="settings-sidebar-button", + child=Box( + orientation="h", + spacing=10, + children=[svg_file(icon_name, size=16), Label(label=label)], + ), + on_clicked=lambda *_: self.set_page(page_name), + ) + setup_cursor_hover(btn, "pointer") + return btn + + def set_page(self, page_name): + if page_name not in self.pages: + return + self.stack.set_visible_child(self.pages[page_name]) + for name, btn in self.sidebar_buttons.items(): + if name == page_name: + btn.add_style_class("active") + else: + btn.remove_style_class("active") + + def _create_general_page(self): + return SettingsPage( + "General Settings", + [ + SettingsRow( + "Wallpapers Directory", + SettingsEntry("wallpapers_dir"), + "Path to your wallpaper collection", + ), + SettingsRow( + "Weather Location", + SettingsEntry("weather_location"), + "City name for weather updates", + ), + SettingsRow( + "Keyboard Layouts", + SettingsList("keyboard_layouts"), + "Manage active keyboard input languages", + ), + SettingsRow( + "Window Switcher", + SettingsSwitch("window_switcher"), + "Enable the Alt-Tab window switcher", + ), + SettingsRow( + "Items Per Row", + SettingsEntry("window_switcher_items_per_row"), + "Maximum items in window switcher row", + ), + SettingsRow( + "Hide Special Workspace", + SettingsSwitch("hide_special_workspace"), + "Don't show special workspace in indicators", + ), + SettingsRow( + "OSD", + SettingsSwitch("osd"), + "On-screen display for volume/brightness", + ), + ], + ) + + def _create_dock_page(self): + return SettingsPage( + "Dock Settings", + [ + SettingsRow( + "Enabled", + SettingsSwitch("dock_enabled"), + "Show the application dock", + ), + SettingsRow( + "Auto Hide", + SettingsSwitch("dock_auto_hide"), + "Hide dock when not in use", + ), + SettingsRow( + "Always Occluded", + SettingsSwitch("dock_always_occluded"), + "Keep dock behind other windows", + ), + SettingsRow( + "Icon Size", + SettingsEntry("dock_icon_size"), + "Size of dock icons in pixels", + ), + SettingsRow( + "Hide Special Apps", + SettingsSwitch("dock_hide_special_workspace_apps"), + "Hide apps from special workspace in dock", + ), + ], + ) + + def _create_panel_page(self): + # panel visibility settings + rows = [] + panel_settings = [ + ("iMac Button", "imac_button"), + ("Global Menu", "global_menu"), + ("Systray", "systray"), + ("Control Center", "control_center"), + ("Search", "search"), + ("Network", "network"), + ("Battery", "battery"), + ("Bluetooth", "bluetooth"), + ("Date & Time", "date_time"), + ("Workspace Indicator", "workspace_indicator"), + ("Notification Center", "notification_center"), + ] + for label, key in panel_settings: + rows.append(SettingsRow(label, SettingsSwitch(key))) + + rows.append( + SettingsRow( + "Systray Ignore", + SettingsList("systray_ignore"), + "Icons to hide from the system tray", + ) + ) + + return SettingsPage("Panel Settings", rows) + + def _create_notifications_page(self): + return SettingsPage( + "Notification Settings", + [ + SettingsRow( + "Timeout", + SettingsEntry("notification_timeout"), + "How long notifications stay on screen (e.g. 5s)", + ), + SettingsRow( + "Ignored Apps", + SettingsList("notification_ignored_apps"), + "Apps that won't show notifications", + ), + SettingsRow( + "Limited History", + SettingsList("notification_limited_apps_history"), + "Apps with only the latest notification shown", + ), + ], + ) + + def _on_window_destroy(self, *args): + global _settings_window + _settings_window = None + + def toggle(self): + if not self.get_visible(): + self.show_all() + self.present() + + # Re-sync current page + current_page = "general" + for name, btn in self.sidebar_buttons.items(): + if "active" in btn.get_style_context().list_classes(): + current_page = name + break + self.set_page(current_page) + else: + self.hide() + + +_settings_window = None + + +def get_settings_window(): + global _settings_window + if _settings_window is None: + _settings_window = SettingsWindow() + return _settings_window diff --git a/src/window/switcher.py b/src/window/switcher.py index 408589f..f8a86e2 100644 --- a/src/window/switcher.py +++ b/src/window/switcher.py @@ -8,7 +8,7 @@ from fabric.widgets.image import Image from fabric.widgets.label import Label from fabric.widgets.wayland import WaylandWindow as Window -import shared.data as data +from services.config import config, on_config_change from utils.functions import is_special_workspace from utils.icon_resolver import IconResolver @@ -31,9 +31,10 @@ class ApplicationSwitcher(Window): self.windows = [] self.current_index = 0 self.tab_pressed = False - self.items_per_row = data.WINDOW_SWITCHER_ITEMS_PER_ROW self.icon_size = 96 - self.items_per_row = 100 # Single row focus + + # Subscribe to config changes + on_config_change(self._on_config_changed) container = Box( name="application-switcher-container", @@ -70,12 +71,25 @@ class ApplicationSwitcher(Window): style_classes=["switcher-workspace-label"], ) container.add(self.workspace_label) + self.connect("key-press-event", self.on_key_press) self.connect("key-release-event", self.on_key_release) self.show_all() self.hide() + @property + def items_per_row(self): + return config().get("window_switcher_items_per_row", 10) + + def _on_config_changed(self, new_config, old_config): + # Refresh windows if settings that affect display change + if config().has_changed( + "hide_special_workspace", old_config + ) or config().has_changed("window_switcher_items_per_row", old_config): + if self.get_visible(): + self.update_windows() + def show_switcher(self) -> None: self.update_windows() if not self.windows: @@ -119,13 +133,13 @@ class ApplicationSwitcher(Window): # Filter out hidden windows and optionally special workspace windows filtered_windows = [] + hide_special = config().get("hide_special_workspace", True) + for c in clients: if c.get("hidden", False): continue # Skip clients in special workspaces if the setting is enabled - if data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS and self._is_special_workspace( - c - ): + if hide_special and self._is_special_workspace(c): continue filtered_windows.append(c) @@ -143,16 +157,24 @@ class ApplicationSwitcher(Window): self.current_index = i break - current_row = Box( - name="window-row", - orientation="h", - spacing=16, - h_align="center", - v_align="center", - ) - self.view.add(current_row) + # Create vertical container for rows + rows_box = Box(orientation="v", spacing=16) + self.view.add(rows_box) + + current_row = None + items_per_row = self.items_per_row for i, window in enumerate(self.windows): + if i % items_per_row == 0: + current_row = Box( + name="window-row", + orientation="h", + spacing=16, + h_align="center", + v_align="center", + ) + rows_box.add(current_row) + # Create icon image for this window icon_image = self.create_icon_for_window(window) @@ -256,25 +278,30 @@ class ApplicationSwitcher(Window): return False def update_selection(self): - for row in self.view.get_children(): - for i, child in enumerate(row.get_children()): - index = self.view.get_children().index(row) * self.items_per_row + i - if index == self.current_index: - child.add_style_class("active") - # Update selection label - current_window = self.windows[self.current_index] - app_class = current_window.get("class", "Unknown") - # capitalize if it's all lowercase - if app_class.islower(): - app_class = app_class.capitalize() - self.selection_label.set_label(app_class) + # Flatten all buttons from all rows to find them by index + all_buttons = [] + rows_box = self.view.get_children()[0] if self.view.get_children() else None + if rows_box: + for row in rows_box.get_children(): + all_buttons.extend(row.get_children()) - # Update workspace label - workspace = current_window.get("workspace", {}) - workspace_name = workspace.get("name", "Unknown") - self.workspace_label.set_label(f"Workspace {workspace_name}") - else: - child.remove_style_class("active") + for i, child in enumerate(all_buttons): + if i == self.current_index: + child.add_style_class("active") + # Update selection label + current_window = self.windows[self.current_index] + app_class = current_window.get("class", "Unknown") + # capitalize if it's all lowercase + if app_class.islower(): + app_class = app_class.capitalize() + self.selection_label.set_label(app_class) + + # Update workspace label + workspace = current_window.get("workspace", {}) + workspace_name = workspace.get("name", "Unknown") + self.workspace_label.set_label(f"Workspace {workspace_name}") + else: + child.remove_style_class("active") def activate_selected(self): if not self.windows or self.current_index >= len(self.windows):