13
13
from textual .css .query import NoMatches
14
14
from textual .message import Message
15
15
from textual .reactive import var
16
+ from textual .timer import Timer
16
17
from textual .widgets import Static
17
18
from textual .widgets ._option_list import Option , OptionList
18
19
@@ -59,6 +60,49 @@ class UpdateSelection(Message):
59
60
option_index : int
60
61
"""The index of the new selection."""
61
62
63
+ def __init__ (self , type_to_search : bool = True ) -> None :
64
+ super ().__init__ ()
65
+ self ._type_to_search = type_to_search
66
+ self ._search_query : str = ""
67
+
68
+ def on_mount (self ) -> None :
69
+ def reset_query () -> None :
70
+ self ._search_query = ""
71
+
72
+ self ._search_reset_timer = Timer (self , 1.0 , callback = reset_query )
73
+
74
+ def watch_has_focus (self , value : bool ) -> None :
75
+ self ._search_query = ""
76
+ if value :
77
+ self ._search_reset_timer ._start ()
78
+ else :
79
+ self ._search_reset_timer .reset ()
80
+ self ._search_reset_timer .stop ()
81
+ super ().watch_has_focus (value )
82
+
83
+ async def _on_key (self , event : events .Key ) -> None :
84
+ if not self ._type_to_search :
85
+ return
86
+
87
+ self ._search_reset_timer .reset ()
88
+
89
+ if event .character is not None and event .is_printable :
90
+ event .time = 0
91
+ event .stop ()
92
+ event .prevent_default ()
93
+
94
+ # Update the search query and jump to the next option that matches.
95
+ self ._search_query += event .character
96
+ index = self ._find_search_match (self ._search_query )
97
+ if index is not None :
98
+ self .select (index )
99
+
100
+ def check_consume_key (self , key : str , character : str | None = None ) -> bool :
101
+ """Check if the widget may consume the given key."""
102
+ return (
103
+ self ._type_to_search and character is not None and character .isprintable ()
104
+ )
105
+
62
106
def select (self , index : int | None ) -> None :
63
107
"""Move selection.
64
108
@@ -68,6 +112,18 @@ def select(self, index: int | None) -> None:
68
112
self .highlighted = index
69
113
self .scroll_to_highlight ()
70
114
115
+ def _find_search_match (self , query : str ) -> int | None :
116
+ """Find the first index"""
117
+ for index , option in enumerate (self ._options ):
118
+ prompt = option .prompt
119
+ if isinstance (prompt , Text ):
120
+ if query in prompt .plain :
121
+ return index
122
+ elif isinstance (prompt , str ):
123
+ if query in prompt :
124
+ return index
125
+ return None
126
+
71
127
def action_dismiss (self ) -> None :
72
128
"""Dismiss the overlay."""
73
129
self .post_message (self .Dismiss ())
@@ -295,6 +351,7 @@ def __init__(
295
351
prompt : str = "Select" ,
296
352
allow_blank : bool = True ,
297
353
value : SelectType | NoSelection = BLANK ,
354
+ type_to_search : bool = True ,
298
355
name : str | None = None ,
299
356
id : str | None = None ,
300
357
classes : str | None = None ,
@@ -313,6 +370,7 @@ def __init__(
313
370
value: Initial value selected. Should be one of the values in `options`.
314
371
If no initial value is set and `allow_blank` is `False`, the widget
315
372
will auto-select the first available option.
373
+ type_to_search: If `True`, typing will search for options.
316
374
name: The name of the select control.
317
375
id: The ID of the control in the DOM.
318
376
classes: The CSS classes of the control.
@@ -327,6 +385,7 @@ def __init__(
327
385
self .prompt = prompt
328
386
self ._value = value
329
387
self ._setup_variables_for_options (options )
388
+ self ._type_to_search = type_to_search
330
389
if tooltip is not None :
331
390
self .tooltip = tooltip
332
391
@@ -338,6 +397,7 @@ def from_values(
338
397
prompt : str = "Select" ,
339
398
allow_blank : bool = True ,
340
399
value : SelectType | NoSelection = BLANK ,
400
+ type_to_search : bool = True ,
341
401
name : str | None = None ,
342
402
id : str | None = None ,
343
403
classes : str | None = None ,
@@ -357,6 +417,7 @@ def from_values(
357
417
value: Initial value selected. Should be one of the values in `values`.
358
418
If no initial value is set and `allow_blank` is `False`, the widget
359
419
will auto-select the first available value.
420
+ type_to_search: If `True`, typing will search for options.
360
421
name: The name of the select control.
361
422
id: The ID of the control in the DOM.
362
423
classes: The CSS classes of the control.
@@ -372,6 +433,7 @@ def from_values(
372
433
prompt = prompt ,
373
434
allow_blank = allow_blank ,
374
435
value = value ,
436
+ type_to_search = type_to_search ,
375
437
name = name ,
376
438
id = id ,
377
439
classes = classes ,
@@ -496,7 +558,7 @@ def _watch_value(self, value: SelectType | NoSelection) -> None:
496
558
def compose (self ) -> ComposeResult :
497
559
"""Compose Select with overlay and current value."""
498
560
yield SelectCurrent (self .prompt )
499
- yield SelectOverlay ()
561
+ yield SelectOverlay (type_to_search = self . _type_to_search )
500
562
501
563
def _on_mount (self , _event : events .Mount ) -> None :
502
564
"""Set initial values."""
0 commit comments