11using System . ComponentModel ;
2+ using System . Diagnostics . CodeAnalysis ;
23using JellyBox . Models ;
34using JellyBox . Services ;
45using JellyBox . ViewModels ;
89using Windows . UI . Xaml ;
910using Windows . UI . Xaml . Controls ;
1011using Windows . UI . Xaml . Input ;
12+ using Windows . UI . Xaml . Media ;
1113using Windows . UI . Xaml . Navigation ;
1214
1315namespace JellyBox ;
1416
1517internal sealed partial class MainPage : Page
1618{
1719 private FrameworkElement ? _lastFocusedElement ;
18- private bool _ignoreNextQuerySubmitted ;
20+ private int _ignoreQuerySubmittedCount ;
1921 private bool _suppressSearchTextSync ;
2022
2123 public MainPage ( )
@@ -25,6 +27,7 @@ public MainPage()
2527 ViewModel = AppServices . Instance . ServiceProvider . GetRequiredService < MainPageViewModel > ( ) ;
2628 ViewModel . IsMenuOpenChanged += OnIsMenuOpenChanged ;
2729 ViewModel . Search . PropertyChanged += OnSearchPropertyChanged ;
30+ PreviewKeyDown += MainPage_PreviewKeyDown ;
2831
2932 // Cache the page state so the ContentFrame's BackStack can be preserved
3033 NavigationCacheMode = NavigationCacheMode . Required ;
@@ -41,6 +44,7 @@ public MainPage()
4144 {
4245 ContentFrame . Navigated -= ContentFrameNavigated ;
4346 ViewModel . Search . PropertyChanged -= OnSearchPropertyChanged ;
47+ PreviewKeyDown -= MainPage_PreviewKeyDown ;
4448 } ;
4549 }
4650
@@ -135,9 +139,48 @@ private void ContentFrameNavigated(object sender, NavigationEventArgs e)
135139
136140 private void SearchBox_LostFocus ( object sender , RoutedEventArgs e )
137141 {
142+ // Keep the search box in the focus order while suggestions are open so the user
143+ // can dismiss the OSK with B and navigate the list with the d-pad.
144+ if ( ViewModel . Search . Suggestions . Count > 0 )
145+ {
146+ return ;
147+ }
148+
138149 SearchBox . IsTabStop = false ;
139150 }
140151
152+ private void MainPage_PreviewKeyDown ( object sender , KeyRoutedEventArgs e )
153+ {
154+ if ( ! IsSearchSuggestionsActive ( ) )
155+ {
156+ return ;
157+ }
158+
159+ if ( GamepadInput . IsBackKey ( e . Key ) )
160+ {
161+ // Dismissing the OSK can spuriously fire QuerySubmitted with the first suggestion.
162+ _ignoreQuerySubmittedCount = 2 ;
163+ return ;
164+ }
165+
166+ if ( GamepadInput . IsAcceptKey ( e . Key ) && TryGetFocusedSuggestion ( out SearchSuggestion ? suggestion ) )
167+ {
168+ // Handle gamepad selection here instead of SuggestionChosen, which throws
169+ // InvalidCastException for custom suggestion items inside AutoSuggestBox on Xbox.
170+ e . Handled = true ;
171+ OpenSearchSuggestion ( suggestion ) ;
172+ }
173+ }
174+
175+ private void SearchSuggestionItem_Tapped ( object sender , TappedRoutedEventArgs e )
176+ {
177+ if ( sender is FrameworkElement element && element . DataContext is SearchSuggestion suggestion )
178+ {
179+ e . Handled = true ;
180+ OpenSearchSuggestion ( suggestion ) ;
181+ }
182+ }
183+
141184 private void CloseNavigation ( object sender , TappedRoutedEventArgs e )
142185 {
143186 ViewModel . CloseNavigationCommand . Execute ( null ) ;
@@ -165,9 +208,9 @@ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChan
165208
166209 private void SearchBox_QuerySubmitted ( AutoSuggestBox sender , AutoSuggestBoxQuerySubmittedEventArgs args )
167210 {
168- if ( _ignoreNextQuerySubmitted )
211+ if ( _ignoreQuerySubmittedCount > 0 )
169212 {
170- _ignoreNextQuerySubmitted = false ;
213+ _ignoreQuerySubmittedCount -- ;
171214 return ;
172215 }
173216
@@ -180,47 +223,74 @@ private void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuery
180223 ViewModel . Search . SubmitQuery ( args . QueryText ) ;
181224 }
182225
183- private void SearchBox_SuggestionChosen ( AutoSuggestBox sender , AutoSuggestBoxSuggestionChosenEventArgs args )
226+ private void OpenSearchSuggestion ( SearchSuggestion suggestion )
184227 {
185- if ( args . SelectedItem is SearchSuggestion suggestion )
186- {
187- OpenSearchSuggestion ( suggestion ) ;
188- }
228+ _ignoreQuerySubmittedCount = 1 ;
229+ ViewModel . Search . SelectSuggestion ( suggestion ) ;
189230 }
190231
191- private void OpenSearchSuggestion ( SearchSuggestion suggestion )
232+ private void ClearSearchField ( )
192233 {
193- _ignoreNextQuerySubmitted = true ;
234+ if ( string . IsNullOrEmpty ( ViewModel . Search . Query ) && string . IsNullOrEmpty ( SearchBox . Text ) )
235+ {
236+ return ;
237+ }
194238
195239 _suppressSearchTextSync = true ;
196240 try
197241 {
242+ ViewModel . Search . ClearQuery ( ) ;
198243 SearchBox . Text = string . Empty ;
199- ViewModel . Search . SelectSuggestion ( suggestion ) ;
200244 }
201245 finally
202246 {
203247 _suppressSearchTextSync = false ;
204248 }
205249 }
206250
207- private void ClearSearchField ( )
251+ private bool IsSearchSuggestionsActive ( )
208252 {
209- if ( string . IsNullOrEmpty ( ViewModel . Search . Query ) && string . IsNullOrEmpty ( SearchBox . Text ) )
253+ if ( ViewModel . Search . Suggestions . Count == 0 )
210254 {
211- return ;
255+ return false ;
212256 }
213257
214- _suppressSearchTextSync = true ;
215- try
258+ return IsSearchBoxFocused ( ) || TryGetFocusedSuggestion ( out _ ) ;
259+ }
260+
261+ private bool IsSearchBoxFocused ( )
262+ {
263+ for ( DependencyObject ? current = FocusManager . GetFocusedElement ( ) as DependencyObject ;
264+ current is not null ;
265+ current = VisualTreeHelper . GetParent ( current ) )
216266 {
217- ViewModel . Search . ClearQuery ( ) ;
218- SearchBox . Text = string . Empty ;
267+ if ( current == SearchBox )
268+ {
269+ return true ;
270+ }
219271 }
220- finally
272+
273+ return false ;
274+ }
275+
276+ private static bool TryGetFocusedSuggestion ( [ NotNullWhen ( true ) ] out SearchSuggestion ? suggestion )
277+ {
278+ suggestion = null ;
279+ if ( FocusManager . GetFocusedElement ( ) is not DependencyObject focused )
221280 {
222- _suppressSearchTextSync = false ;
281+ return false ;
223282 }
283+
284+ for ( DependencyObject ? current = focused ; current is not null ; current = VisualTreeHelper . GetParent ( current ) )
285+ {
286+ if ( current is FrameworkElement { DataContext : SearchSuggestion context } )
287+ {
288+ suggestion = context ;
289+ return true ;
290+ }
291+ }
292+
293+ return false ;
224294 }
225295
226296 internal sealed record Parameters ( Action DeferredNavigationAction ) ;
0 commit comments