Skip to content

Commit bcdd623

Browse files
Fix shell search suggestion pick after dismissing OSK with B
AutoSuggestBox.SuggestionChosen throws InvalidCastException for custom suggestion items on Xbox. Handle gamepad A/Enter via PreviewKeyDown, mouse/touch via Tapped, and keyboard via QuerySubmitted.ChosenSuggestion. Suppress spurious QuerySubmitted after B dismisses the OSK.
1 parent 9d055d4 commit bcdd623

2 files changed

Lines changed: 96 additions & 22 deletions

File tree

src/JellyBox/MainPage.xaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@
2727
IsTabStop="False"
2828
PlaceholderText="Search movies and TV shows"
2929
Style="{StaticResource SearchAutoSuggestBox}"
30+
UpdateTextOnSelect="False"
3031
ItemsSource="{x:Bind ViewModel.Search.Suggestions, Mode=OneWay}"
3132
TextChanged="SearchBox_TextChanged"
3233
QuerySubmitted="SearchBox_QuerySubmitted"
33-
SuggestionChosen="SearchBox_SuggestionChosen"
3434
LostFocus="SearchBox_LostFocus"
3535
XYFocusKeyboardNavigation="Enabled">
3636
<AutoSuggestBox.QueryIcon>
3737
<SymbolIcon Symbol="Find" />
3838
</AutoSuggestBox.QueryIcon>
3939
<AutoSuggestBox.ItemTemplate>
4040
<DataTemplate x:DataType="models:SearchSuggestion">
41-
<StackPanel Orientation="Horizontal" Spacing="12" Padding="4,10">
41+
<StackPanel
42+
Orientation="Horizontal"
43+
Spacing="12"
44+
Padding="4,10"
45+
Tapped="SearchSuggestionItem_Tapped">
4246
<FontIcon
4347
FontFamily="{StaticResource SegoeIcons}"
4448
Glyph="{x:Bind g:Glyphs.Search}"

src/JellyBox/MainPage.xaml.cs

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
23
using JellyBox.Models;
34
using JellyBox.Services;
45
using JellyBox.ViewModels;
@@ -8,14 +9,15 @@
89
using Windows.UI.Xaml;
910
using Windows.UI.Xaml.Controls;
1011
using Windows.UI.Xaml.Input;
12+
using Windows.UI.Xaml.Media;
1113
using Windows.UI.Xaml.Navigation;
1214

1315
namespace JellyBox;
1416

1517
internal 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

Comments
 (0)