From f8579337480a7e92b6b2a53c47e6445a9d4ad410 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:27:03 -0400 Subject: [PATCH] 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 Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Launcher.py | 31 +++++++++++++++++++++++++++++-- data/launcher.kv | 32 ++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Launcher.py b/Launcher.py index 29bd7176..713c0cd3 100644 --- a/Launcher.py +++ b/Launcher.py @@ -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. diff --git a/data/launcher.kv b/data/launcher.kv index 8c6a8288..1cb4e84a 100644 --- a/data/launcher.kv +++ b/data/launcher.kv @@ -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 \ No newline at end of file + 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