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.core.window import Window
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.menu import MDDropdownMenu
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
from kivymd.uix.textfield import MDTextField
from kivy.lang.builder import Builder
@@ -253,6 +254,7 @@ def run_gui(path: str, args: Any) -> None:
navigation: MDGridLayout = ObjectProperty(None)
grid: MDGridLayout = ObjectProperty(None)
button_layout: ScrollBox = ObjectProperty(None)
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard]
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)
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.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):
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
self.grid = self.top_screen.ids.grid
self.navigation = self.top_screen.ids.navigation
self.button_layout = self.top_screen.ids.button_layout
self.search_box = self.top_screen.ids.search_box
self.set_colors()
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
Window.bind(on_drop_file=self._on_drop_file)
Window.bind(on_keyboard=self._on_keyboard)
for component in components:
self.cards.append(self.build_card(component))
@@ -389,6 +407,15 @@ def run_gui(path: str, args: Any) -> None:
else:
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):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up.

View File

@@ -80,7 +80,7 @@ MDFloatLayout:
id: all
style: "text"
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "asterisk"
@@ -90,7 +90,7 @@ MDFloatLayout:
id: client
style: "text"
type: (Type.CLIENT, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "controller"
@@ -100,7 +100,7 @@ MDFloatLayout:
id: Tool
style: "text"
type: (Type.TOOL, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "desktop-classic"
@@ -110,7 +110,7 @@ MDFloatLayout:
id: adjuster
style: "text"
type: (Type.ADJUSTER, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "wrench"
@@ -120,7 +120,7 @@ MDFloatLayout:
id: misc
style: "text"
type: (Type.MISC, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "dots-horizontal-circle-outline"
@@ -131,7 +131,7 @@ MDFloatLayout:
id: favorites
style: "text"
type: ("favorites", )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "star"
@@ -141,5 +141,21 @@ MDFloatLayout:
MDNavigationDrawerDivider:
ScrollBox:
id: button_layout
MDGridLayout:
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