155 lines
5.8 KiB
Python
155 lines
5.8 KiB
Python
"""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()
|