"""Section 5: the full-width application drawer that replaces nwg-drawer. Top to bottom: a favourites row (full-width module), then search, then a FlowBox of all apps. Collapsed it is a bottom strip; expanded it fills down to the bottom. Right-click / long-press an app tile to pin or unpin it from favourites. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") gi.require_version("AstalApps", "0.1") from gi.repository import AstalApps, Gtk # noqa: E402 from ui.favorites import Favorites _STRIP_HEIGHT = 190 # collapsed grid height (logical px) class AppDrawer(Gtk.Box): def __init__(self, settings, on_launch, on_toggle_expand): super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8) self.add_css_class("appdrawer") self.settings = settings self._on_launch = on_launch self._on_toggle_expand = on_toggle_expand self._apps = AstalApps.Apps() self._expanded = False self.favorites = Favorites(settings, on_launch) self.append(self.favorites) self.append(Gtk.Separator()) self.append(self._build_header()) self._search = Gtk.SearchEntry(placeholder_text="Search applications") self._search.add_css_class("appdrawer-search") self._search.connect("search-changed", self._on_search) self.append(self._search) self._flow = Gtk.FlowBox( selection_mode=Gtk.SelectionMode.NONE, homogeneous=True, min_children_per_line=4, max_children_per_line=12, row_spacing=8, column_spacing=8, valign=Gtk.Align.START) # keep rows their natural height (no stretch) self._flow.add_css_class("appdrawer-flow") self._flow.connect("child-activated", self._on_child_activated) self._scroll = Gtk.ScrolledWindow( hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True) self._scroll.set_propagate_natural_height(False) self._scroll.set_child(self._flow) self.append(self._scroll) self._apply_mode() self._populate(self._sorted_all()) def _build_header(self) -> Gtk.Widget: header = Gtk.CenterBox() header.add_css_class("appdrawer-header") title = Gtk.Label(label="Applications", xalign=0.0) title.add_css_class("section-title") header.set_start_widget(title) self._expand_btn = Gtk.Button(label="") # chevron up self._expand_btn.add_css_class("quad-action") self._expand_btn.set_tooltip_text("Expand") self._expand_btn.connect("clicked", lambda *_: self._toggle_expand()) header.set_end_widget(self._expand_btn) return header # -- expansion --------------------------------------------------------- def _apply_mode(self) -> None: if self._expanded: self.set_vexpand(True) self._scroll.set_min_content_height(_STRIP_HEIGHT) self._scroll.set_max_content_height(100000) self._expand_btn.set_label("") # chevron down else: self.set_vexpand(False) self._scroll.set_min_content_height(_STRIP_HEIGHT) self._scroll.set_max_content_height(_STRIP_HEIGHT) self._expand_btn.set_label("") # chevron up def _toggle_expand(self) -> None: self._expanded = not self._expanded self._apply_mode() self._on_toggle_expand(self._expanded) def set_expanded(self, value: bool) -> None: if value != self._expanded: self._toggle_expand() # -- data -------------------------------------------------------------- def _sorted_all(self) -> list: apps = list(self._apps.get_list()) apps.sort(key=lambda a: (-a.get_frequency(), a.get_name().lower())) return apps def _on_search(self, entry: Gtk.SearchEntry) -> None: text = entry.get_text().strip() results = self._apps.fuzzy_query(text) if text else self._sorted_all() self._populate(results) def _populate(self, apps: list) -> None: child = self._flow.get_first_child() while child: self._flow.remove(child) child = self._flow.get_first_child() for app in apps: self._flow.append(self._app_button(app)) def _app_button(self, app) -> Gtk.Widget: btn = Gtk.Button(valign=Gtk.Align.START) btn.add_css_class("app-tile") btn.app = app content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) icon = Gtk.Image.new_from_icon_name(app.get_icon_name() or "application-x-executable") icon.set_pixel_size(48) label = Gtk.Label(label=app.get_name(), ellipsize=3, max_width_chars=12, justify=Gtk.Justification.CENTER) content.append(icon) content.append(label) btn.set_child(content) btn.connect("clicked", lambda *_: self._launch(app)) # pin / unpin: right-click or long-press rclick = Gtk.GestureClick(button=3) rclick.connect("released", lambda *_a: self._toggle_fav(app)) btn.add_controller(rclick) longpress = Gtk.GestureLongPress() longpress.connect("pressed", lambda *_a: self._toggle_fav(app)) btn.add_controller(longpress) return btn def _toggle_fav(self, app) -> None: self.settings.toggle_favorite(app.get_entry()) def _on_child_activated(self, _flow, child) -> None: btn = child.get_child() if btn and getattr(btn, "app", None): self._launch(btn.app) def _launch(self, app) -> None: try: app.launch() except Exception: pass self._on_launch() def on_show(self) -> None: self._search.set_text("") self._populate(self._sorted_all()) self.favorites.refresh() self._search.grab_focus()