Launcher: Add search box (#4863)

* Add fuzzy search box to Launcher.

* move func bind to the kv and prefer substring matching (#79)

* move the func bind to the kv

* prefer substr matching

* Remove fuzzy results, rely on substring only.

* Use early return instead of else.

* Add type hint to filter_clients_by_type.

* Activate search on keyboard input.

* Clear search box when filtering by type.

* Update Launcher.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
massimilianodelliubaldini
2025-04-19 17:27:03 -04:00
committed by GitHub
parent efe2b7c539
commit f857933748
2 changed files with 53 additions and 10 deletions

View File

@@ -230,10 +230,11 @@ def run_gui(path: str, args: Any) -> None:
from kivy.properties import ObjectProperty from kivy.properties import ObjectProperty
from kivy.core.window import Window from kivy.core.window import Window
from kivy.metrics import dp from kivy.metrics import dp
from kivymd.uix.button import MDIconButton from kivymd.uix.button import MDIconButton, MDButton
from kivymd.uix.card import MDCard from kivymd.uix.card import MDCard
from kivymd.uix.menu import MDDropdownMenu from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
from kivymd.uix.textfield import MDTextField
from kivy.lang.builder import Builder from kivy.lang.builder import Builder
@@ -253,6 +254,7 @@ def run_gui(path: str, args: Any) -> None:
navigation: MDGridLayout = ObjectProperty(None) navigation: MDGridLayout = ObjectProperty(None)
grid: MDGridLayout = ObjectProperty(None) grid: MDGridLayout = ObjectProperty(None)
button_layout: ScrollBox = ObjectProperty(None) button_layout: ScrollBox = ObjectProperty(None)
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard] cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None current_filter: Sequence[str | Type] | None
@@ -338,14 +340,29 @@ def run_gui(path: str, args: Any) -> None:
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top) scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1])) self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
def filter_clients(self, caller): def filter_clients_by_type(self, caller: MDButton):
self._refresh_components(caller.type) self._refresh_components(caller.type)
self.search_box.text = ""
def filter_clients_by_name(self, caller: MDTextField, name: str) -> None:
if len(name) == 0:
self._refresh_components(self.current_filter)
return
sub_matches = [
card for card in self.cards
if name.lower() in card.component.display_name.lower() and card.component.type != Type.HIDDEN
]
self.button_layout.layout.clear_widgets()
for card in sub_matches:
self.button_layout.layout.add_widget(card)
def build(self): def build(self):
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv")) self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
self.grid = self.top_screen.ids.grid self.grid = self.top_screen.ids.grid
self.navigation = self.top_screen.ids.navigation self.navigation = self.top_screen.ids.navigation
self.button_layout = self.top_screen.ids.button_layout self.button_layout = self.top_screen.ids.button_layout
self.search_box = self.top_screen.ids.search_box
self.set_colors() self.set_colors()
self.top_screen.md_bg_color = self.theme_cls.backgroundColor self.top_screen.md_bg_color = self.theme_cls.backgroundColor
@@ -353,6 +370,7 @@ def run_gui(path: str, args: Any) -> None:
refresh_components = self._refresh_components refresh_components = self._refresh_components
Window.bind(on_drop_file=self._on_drop_file) Window.bind(on_drop_file=self._on_drop_file)
Window.bind(on_keyboard=self._on_keyboard)
for component in components: for component in components:
self.cards.append(self.build_card(component)) self.cards.append(self.build_card(component))
@@ -389,6 +407,15 @@ def run_gui(path: str, args: Any) -> None:
else: else:
logging.warning(f"unable to identify component for {file}") logging.warning(f"unable to identify component for {file}")
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
# Focus first, then capture the first character we type, otherwise it gets swallowed and lost.
# Limit text input to ASCII non-control characters (space bar to tilde).
if not self.search_box.focus:
self.search_box.focus = True
if key in range(32, 126):
self.search_box.text += codepoint
def _stop(self, *largs): def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up. # Closing the window explicitly cleans it up.

View File

@@ -80,7 +80,7 @@ MDFloatLayout:
id: all id: all
style: "text" style: "text"
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC) type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "asterisk" icon: "asterisk"
@@ -90,7 +90,7 @@ MDFloatLayout:
id: client id: client
style: "text" style: "text"
type: (Type.CLIENT, ) type: (Type.CLIENT, )
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "controller" icon: "controller"
@@ -100,7 +100,7 @@ MDFloatLayout:
id: Tool id: Tool
style: "text" style: "text"
type: (Type.TOOL, ) type: (Type.TOOL, )
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "desktop-classic" icon: "desktop-classic"
@@ -110,7 +110,7 @@ MDFloatLayout:
id: adjuster id: adjuster
style: "text" style: "text"
type: (Type.ADJUSTER, ) type: (Type.ADJUSTER, )
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "wrench" icon: "wrench"
@@ -120,7 +120,7 @@ MDFloatLayout:
id: misc id: misc
style: "text" style: "text"
type: (Type.MISC, ) type: (Type.MISC, )
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "dots-horizontal-circle-outline" icon: "dots-horizontal-circle-outline"
@@ -131,7 +131,7 @@ MDFloatLayout:
id: favorites id: favorites
style: "text" style: "text"
type: ("favorites", ) type: ("favorites", )
on_release: app.filter_clients(self) on_release: app.filter_clients_by_type(self)
MDButtonIcon: MDButtonIcon:
icon: "star" icon: "star"
@@ -141,5 +141,21 @@ MDFloatLayout:
MDNavigationDrawerDivider: MDNavigationDrawerDivider:
ScrollBox: MDGridLayout:
id: button_layout id: main_layout
cols: 1
spacing: "10dp"
MDTextField:
id: search_box
mode: "outlined"
set_text: app.filter_clients_by_name
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search"
ScrollBox:
id: button_layout