"""Full-width favourites row at the top of the app drawer. Shows pinned apps (settings["favorites"], a list of .desktop entry ids). If nothing is pinned yet it falls back to the most-frequently-launched apps. Pin/unpin from the drawer grid via right-click / long-press. """ 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 def add_pin_gestures(widget: Gtk.Widget, on_toggle) -> None: """Wire right-click and long-press on `widget` to pin/unpin. Uses the CAPTURE phase and claims the sequence so the gesture fires reliably on a Gtk.Button (whose own primary-click gesture would otherwise swallow it) and does not also trigger the button's launch action.""" def fire(gesture, *_a) -> None: on_toggle() gesture.set_state(Gtk.EventSequenceState.CLAIMED) rclick = Gtk.GestureClick(button=3) rclick.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) rclick.connect("pressed", fire) widget.add_controller(rclick) longpress = Gtk.GestureLongPress() longpress.set_touch_only(False) longpress.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) longpress.connect("pressed", fire) widget.add_controller(longpress) class Favorites(Gtk.Box): def __init__(self, settings, on_launch): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.add_css_class("favorites") self.settings = settings self._on_launch = on_launch self._apps = AstalApps.Apps() title = Gtk.Label(label="Favorites", xalign=0.0) title.add_css_class("section-title") self.append(title) self._row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) self._row.add_css_class("favorites-row") scroller = Gtk.ScrolledWindow( vscrollbar_policy=Gtk.PolicyType.NEVER, hscrollbar_policy=Gtk.PolicyType.AUTOMATIC) scroller.set_child(self._row) self.append(scroller) settings.subscribe(self.refresh) self.refresh() def _by_entry(self) -> dict: return {a.get_entry(): a for a in self._apps.get_list() if a.get_entry()} def _resolve(self) -> list: by_entry = self._by_entry() entries = self.settings.favorites() if entries: return [by_entry[e] for e in entries if e in by_entry] # fallback: most-used apps apps = sorted(self._apps.get_list(), key=lambda a: -a.get_frequency()) return [a for a in apps if a.get_frequency() > 0][:8] def refresh(self) -> None: child = self._row.get_first_child() while child: self._row.remove(child) child = self._row.get_first_child() apps = self._resolve() if not apps: self._row.append(Gtk.Label(label="Right-click an app below to pin it", xalign=0.0)) return for app in apps: self._row.append(self._tile(app)) def _tile(self, app) -> Gtk.Widget: btn = Gtk.Button() btn.add_css_class("fav-tile") content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) icon = Gtk.Image.new_from_icon_name(app.get_icon_name() or "application-x-executable") icon.set_pixel_size(28) content.append(icon) content.append(Gtk.Label(label=app.get_name(), ellipsize=3, max_width_chars=14)) btn.set_child(content) btn.connect("clicked", lambda *_a: self._launch(app)) # right-click / long-press unpins add_pin_gestures(btn, lambda: self.settings.toggle_favorite(app.get_entry())) return btn def _launch(self, app) -> None: try: app.launch() except Exception: pass self._on_launch()