4
4
5
5
from typing_extensions import TypeGuard
6
6
7
- from textual . await_remove import AwaitRemove
8
- from textual . binding import Binding , BindingType
9
- from textual . containers import VerticalScroll
10
- from textual . events import Mount
11
- from textual . geometry import clamp
12
- from textual .message import Message
13
- from textual .reactive import reactive
14
- from textual . widget import AwaitMount , Widget
15
- from textual .widgets ._list_item import ListItem
7
+ from .. import _widget_navigation
8
+ from .. await_remove import AwaitRemove
9
+ from .. binding import Binding , BindingType
10
+ from .. containers import VerticalScroll
11
+ from .. events import Mount
12
+ from . .message import Message
13
+ from . .reactive import reactive
14
+ from .. widget import AwaitMount
15
+ from . .widgets ._list_item import ListItem
16
16
17
17
18
18
class ListView (VerticalScroll , can_focus = True , can_focus_children = False ):
@@ -38,7 +38,7 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
38
38
| down | Move the cursor down. |
39
39
"""
40
40
41
- index = reactive [Optional [int ]](0 , always_update = True )
41
+ index = reactive [Optional [int ]](0 , always_update = True , init = False )
42
42
"""The index of the currently highlighted item."""
43
43
44
44
class Highlighted (Message ):
@@ -117,7 +117,12 @@ def __init__(
117
117
super ().__init__ (
118
118
* children , name = name , id = id , classes = classes , disabled = disabled
119
119
)
120
- self ._index = initial_index
120
+ # Set the index to the given initial index, or the first available index after.
121
+ self ._index = _widget_navigation .find_next_enabled (
122
+ self ._nodes ,
123
+ anchor = initial_index - 1 if initial_index is not None else None ,
124
+ direction = 1 ,
125
+ )
121
126
122
127
def _on_mount (self , _ : Mount ) -> None :
123
128
"""Ensure the ListView is fully-settled after mounting."""
@@ -142,17 +147,17 @@ def validate_index(self, index: int | None) -> int | None:
142
147
Returns:
143
148
The clamped index.
144
149
"""
145
- if not self . _nodes or index is None :
150
+ if index is None or not self . _nodes :
146
151
return None
147
- return self ._clamp_index (index )
152
+ elif index < 0 :
153
+ return 0
154
+ elif index >= len (self ._nodes ):
155
+ return len (self ._nodes ) - 1
148
156
149
- def _clamp_index (self , index : int ) -> int :
150
- """Clamp the index to a valid value given the current list of children"""
151
- last_index = max (len (self ._nodes ) - 1 , 0 )
152
- return clamp (index , 0 , last_index )
157
+ return index
153
158
154
159
def _is_valid_index (self , index : int | None ) -> TypeGuard [int ]:
155
- """Return True if the current index is valid given the current list of children"""
160
+ """Determine whether the current index is valid into the list of children. """
156
161
if index is None :
157
162
return False
158
163
return 0 <= index < len (self ._nodes )
@@ -164,16 +169,14 @@ def watch_index(self, old_index: int | None, new_index: int | None) -> None:
164
169
assert isinstance (old_child , ListItem )
165
170
old_child .highlighted = False
166
171
167
- new_child : Widget | None
168
- if self ._is_valid_index (new_index ):
172
+ if self ._is_valid_index (new_index ) and not self ._nodes [new_index ].disabled :
169
173
new_child = self ._nodes [new_index ]
170
174
assert isinstance (new_child , ListItem )
171
175
new_child .highlighted = True
176
+ self ._scroll_highlighted_region ()
177
+ self .post_message (self .Highlighted (self , new_child ))
172
178
else :
173
- new_child = None
174
-
175
- self ._scroll_highlighted_region ()
176
- self .post_message (self .Highlighted (self , new_child ))
179
+ self .post_message (self .Highlighted (self , None ))
177
180
178
181
def extend (self , items : Iterable [ListItem ]) -> AwaitMount :
179
182
"""Append multiple new ListItems to the end of the ListView.
@@ -222,19 +225,30 @@ def action_select_cursor(self) -> None:
222
225
223
226
def action_cursor_down (self ) -> None :
224
227
"""Highlight the next item in the list."""
225
- if self .index is None :
226
- self .index = 0
227
- return
228
- self .index += 1
228
+ candidate = _widget_navigation .find_next_enabled (
229
+ self ._nodes ,
230
+ anchor = self .index ,
231
+ direction = 1 ,
232
+ )
233
+ if self .index is not None and candidate is not None and candidate < self .index :
234
+ return # Avoid wrapping around.
235
+
236
+ self .index = candidate
229
237
230
238
def action_cursor_up (self ) -> None :
231
239
"""Highlight the previous item in the list."""
232
- if self .index is None :
233
- self .index = 0
234
- return
235
- self .index -= 1
240
+ candidate = _widget_navigation .find_next_enabled (
241
+ self ._nodes ,
242
+ anchor = self .index ,
243
+ direction = - 1 ,
244
+ )
245
+ if self .index is not None and candidate is not None and candidate > self .index :
246
+ return # Avoid wrapping around.
247
+
248
+ self .index = candidate
236
249
237
250
def _on_list_item__child_clicked (self , event : ListItem ._ChildClicked ) -> None :
251
+ event .stop ()
238
252
self .focus ()
239
253
self .index = self ._nodes .index (event .item )
240
254
self .post_message (self .Selected (self , event .item ))
@@ -244,5 +258,6 @@ def _scroll_highlighted_region(self) -> None:
244
258
if self .highlighted_child is not None :
245
259
self .scroll_to_widget (self .highlighted_child , animate = False )
246
260
247
- def __len__ (self ):
261
+ def __len__ (self ) -> int :
262
+ """Compute the length (in number of items) of the list view."""
248
263
return len (self ._nodes )
0 commit comments