Skip to content

Feature: Added option to filter items when typing #17339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public Task ExecuteAsync(object? parameter = null)

if (IsOn)
ContentPageContext.ShellPage!.ShellViewModel.InvokeFocusFilterHeader();
else
{
// Clear the filter query when the header is hidden
ContentPageContext.ShellPage!.ShellViewModel.FilesAndFoldersFilter = string.Empty;
}

return Task.CompletedTask;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Files.App/Data/Contracts/IFoldersSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,10 @@ public interface IFoldersSettingsService : IBaseSettingsService, INotifyProperty
/// Gets or sets a value indicating which format to use when displaying item sizes.
/// </summary>
SizeUnitTypes SizeUnitFormat { get; set; }

/// <summary>
/// Gets or sets a value indicating the keyboard typing behavior.
/// </summary>
KeyboardTypingBehavior KeyboardTypingBehavior { get; set; }
}
}
18 changes: 18 additions & 0 deletions src/Files.App/Data/Enums/KeyboardTypingBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

namespace Files.App.Data.Enums
{
public enum KeyboardTypingBehavior
{
/// <summary>
/// Jump to matching item.
/// </summary>
JumpToFile,

/// <summary>
/// Filter items.
/// </summary>
FilterItems
}
}
7 changes: 7 additions & 0 deletions src/Files.App/Services/Settings/FoldersSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ public SizeUnitTypes SizeUnitFormat
set => Set(value);
}

/// <inheritdoc/>
public KeyboardTypingBehavior KeyboardTypingBehavior
{
get => (KeyboardTypingBehavior)Get((long)KeyboardTypingBehavior.JumpToFile);
set => Set((long)value);
}

protected override void RaiseOnSettingChangedEvent(object sender, SettingChangedEventArgs e)
{
base.RaiseOnSettingChangedEvent(sender, e);
Expand Down
9 changes: 9 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -4278,6 +4278,15 @@
</data>
<data name="Filename" xml:space="preserve">
<value>Filename</value>
</data>
<data name="KeyboardTypingBehavior" xml:space="preserve">
<value>Behavior when typing in the file area</value>
</data>
<data name="JumpToFile" xml:space="preserve">
<value>Jump to file</value>
</data>
<data name="FilterItems" xml:space="preserve">
<value>Filter items</value>
</data>
<data name="Signatures" xml:space="preserve">
<value>Signatures</value>
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ private static char[] RestrictedCharacters
{
var userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
return userSettingsService.FoldersSettingsService.AreAlternateStreamsVisible
? ['\\', '/', '*', '?', '"', '<', '>', '|'] // Allow ":" char
: ['\\', '/', ':', '*', '?', '"', '<', '>', '|'];
? Path.GetInvalidFileNameChars().Where(c => c != ':').ToArray() // Allow ":" char when alternate streams are visible
: Path.GetInvalidFileNameChars();
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/Files.App/ViewModels/Settings/AdvancedViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public sealed partial class AdvancedViewModel : ObservableObject
public ICommand ImportSettingsCommand { get; }
public AsyncRelayCommand OpenFilesOnWindowsStartupCommand { get; }

public Dictionary<KeyboardTypingBehavior, string> KeyboardTypingBehaviors { get; private set; } = [];

public AdvancedViewModel()
{
Expand All @@ -40,6 +41,11 @@ public AdvancedViewModel()
ImportSettingsCommand = new AsyncRelayCommand(ImportSettingsAsync);
OpenFilesOnWindowsStartupCommand = new AsyncRelayCommand(OpenFilesOnWindowsStartupAsync);

// Keyboard typing behavior
KeyboardTypingBehaviors.Add(Data.Enums.KeyboardTypingBehavior.JumpToFile, Strings.JumpToFile.GetLocalizedResource());
KeyboardTypingBehaviors.Add(Data.Enums.KeyboardTypingBehavior.FilterItems, Strings.FilterItems.GetLocalizedResource());
KeyboardTypingBehavior = KeyboardTypingBehaviors[UserSettingsService.FoldersSettingsService.KeyboardTypingBehavior];

_ = DetectOpenFilesAtStartupAsync();
}

Expand Down Expand Up @@ -354,6 +360,20 @@ public bool ShowFlattenOptions
OnPropertyChanged();
}
}

private string keyboardTypingBehavior;
public string KeyboardTypingBehavior
{
get => keyboardTypingBehavior;
set
{
if (SetProperty(ref keyboardTypingBehavior, value))
{
UserSettingsService.FoldersSettingsService.KeyboardTypingBehavior = KeyboardTypingBehaviors.First(e => e.Value == value).Key;
}
}
}

public async Task OpenFilesOnWindowsStartupAsync()
{
var stateMode = await ReadState();
Expand Down
37 changes: 35 additions & 2 deletions src/Files.App/ViewModels/ShellViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,9 +777,42 @@ public void UpdateEmptyTextType()
public string? FilesAndFoldersFilter
{
get => _filesAndFoldersFilter;
set => SetProperty(ref _filesAndFoldersFilter, value);
set
{
if (SetProperty(ref _filesAndFoldersFilter, value))
{
// Apply the updated filter to the files and folders list
FilesAndFolderFilterUpdated();
}
}
}

private void FilesAndFolderFilterUpdated()
{
_ = ApplyFilesAndFoldersChangesAsync();
}

/// <summary>
/// Clears the files and folder filter.
/// This is used when the directory is changed or refreshed.
/// </summary>
private void ClearFilesAndFolderFilter()
{
// Hide the filter header if:
// - Keyboard behavior is set to filter items
// - A filter is currently applied
//
// Keep the header visible if:
// - The filter is already empty (e.g. opened manually)
if (UserSettingsService.FoldersSettingsService.KeyboardTypingBehavior == KeyboardTypingBehavior.FilterItems &&
!string.IsNullOrEmpty(FilesAndFoldersFilter))
{
UserSettingsService.GeneralSettingsService.ShowFilterHeader = false;
}

// Clear the filter
FilesAndFoldersFilter = string.Empty;
}

// Apply changes immediately after manipulating on filesAndFolders completed
public async Task ApplyFilesAndFoldersChangesAsync()
Expand Down Expand Up @@ -1894,7 +1927,6 @@ await Task.Run(async () =>
});

filesAndFolders.AddRange(fileList);
FilesAndFoldersFilter = null;

await OrderFilesAndFoldersAsync();
await ApplyFilesAndFoldersChangesAsync();
Expand All @@ -1903,6 +1935,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
{
GetDesktopIniFileData();
CheckForBackgroundImage();
ClearFilesAndFolderFilter();
},
Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
});
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ protected override void UnhookEvents()
ItemManipulationModel.RefreshItemsThumbnailInvoked -= ItemManipulationModel_RefreshItemsThumbnail;
}

protected override void Page_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
protected override void Page_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (ParentShellPageInstance is null ||
ParentShellPageInstance.CurrentPageType != this.GetType() ||
Expand All @@ -112,7 +112,7 @@ focusedElement is PasswordBox ||
DependencyObjectHelpers.FindParent<ContentDialog>(focusedElement) is not null)
return;

base.Page_CharacterReceived(sender, args);
base.Page_PreviewKeyDown(sender, e);
}

// Virtual methods
Expand Down
86 changes: 80 additions & 6 deletions src/Files.App/Views/Layouts/BaseLayoutPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Vanara.Extensions;
using Vanara.PInvoke;
using Windows.ApplicationModel.DataTransfer;
Expand All @@ -23,6 +24,7 @@
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.System;
using Windows.Win32;
using static Files.App.Helpers.PathNormalization;
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
using SortDirection = Files.App.Data.Enums.SortDirection;
Expand All @@ -40,6 +42,8 @@ public abstract class BaseLayoutPage : Page, IBaseLayoutPage, INotifyPropertyCha
protected IFileTagsSettingsService FileTagsSettingsService { get; } = Ioc.Default.GetService<IFileTagsSettingsService>()!;
protected IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetService<IUserSettingsService>()!;
protected ILayoutSettingsService LayoutSettingsService { get; } = Ioc.Default.GetService<ILayoutSettingsService>()!;
protected IGeneralSettingsService GeneralSettingsService { get; } = Ioc.Default.GetService<IGeneralSettingsService>()!;
protected IFoldersSettingsService FoldersSettingsService { get; } = Ioc.Default.GetService<IFoldersSettingsService>()!;
protected ICommandManager Commands { get; } = Ioc.Default.GetRequiredService<ICommandManager>();
public InfoPaneViewModel InfoPaneViewModel { get; } = Ioc.Default.GetRequiredService<InfoPaneViewModel>();
protected readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService<IWindowContext>();
Expand Down Expand Up @@ -401,7 +405,7 @@ protected override async void OnNavigatedTo(NavigationEventArgs e)
base.OnNavigatedTo(e);

// Add item jumping handler
CharacterReceived += Page_CharacterReceived;
PreviewKeyDown += Page_PreviewKeyDown;

navigationArguments = (NavigationArguments)e.Parameter;
ParentShellPageInstance = navigationArguments.AssociatedTabInstance;
Expand Down Expand Up @@ -565,7 +569,7 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
base.OnNavigatingFrom(e);

// Remove item jumping handler
CharacterReceived -= Page_CharacterReceived;
PreviewKeyDown -= Page_PreviewKeyDown;
FolderSettings!.LayoutModeChangeRequested -= BaseFolderSettings_LayoutModeChangeRequested;
FolderSettings.GroupOptionPreferenceUpdated -= FolderSettings_GroupOptionPreferenceUpdated;
FolderSettings.GroupDirectionPreferenceUpdated -= FolderSettings_GroupDirectionPreferenceUpdated;
Expand Down Expand Up @@ -996,12 +1000,82 @@ private void RemoveOverflow(CommandBarFlyout contextMenuFlyout)
overflowSeparator.Visibility = Visibility.Collapsed;
}

protected virtual void Page_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
protected virtual void Page_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (ParentShellPageInstance!.IsCurrentInstance)
var shellPage = ParentShellPageInstance;
if (shellPage?.IsCurrentInstance != true)
return;

var pressedKey = e.Key;
var currentFilter = shellPage.ShellViewModel.FilesAndFoldersFilter ?? string.Empty;
var isFilterModeOn = FoldersSettingsService.KeyboardTypingBehavior == KeyboardTypingBehavior.FilterItems;
var buffer = new StringBuilder(4);
var state = new byte[256];
char? typedCharacter = null;

if (PInvoke.GetKeyboardState(state))
{
var virtualKey = (uint)pressedKey;
var scanCode = PInvoke.MapVirtualKey(virtualKey, 0);
var keyboardLayout = PInvoke.GetKeyboardLayout(0);

if (Win32PInvoke.ToUnicodeEx(virtualKey, scanCode, state, buffer, buffer.Capacity, 0, keyboardLayout) > 0)
Copy link
Preview

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses Win32PInvoke.ToUnicodeEx but imports Windows.Win32.PInvoke. There's an inconsistency in the API usage - it should be PInvoke.ToUnicodeEx to match the other PInvoke calls in the method.

Suggested change
if (Win32PInvoke.ToUnicodeEx(virtualKey, scanCode, state, buffer, buffer.Capacity, 0, keyboardLayout) > 0)
if (PInvoke.ToUnicodeEx(virtualKey, scanCode, state, buffer, buffer.Capacity, 0, keyboardLayout) > 0)

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fyi @0x5bfa

{
var character = buffer[^1];
Comment on lines +1022 to +1024
Copy link
Preview

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using buffer[^1] assumes the buffer has at least one character, but this isn't guaranteed even when ToUnicodeEx returns > 0. The method could return a negative value for dead keys or zero for no translation. Consider using buffer[0] instead or add proper bounds checking.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (character != ' ')
typedCharacter = character;
}
}

if (!typedCharacter.HasValue)
return;

// Handle valid character input
if (!FilesystemHelpers.ContainsRestrictedCharacters(char.ToString(typedCharacter.Value)))
{
var lowerCharString = char.ToLowerInvariant(typedCharacter.Value).ToString();

if (isFilterModeOn)
{
if (!GeneralSettingsService.ShowFilterHeader)
GeneralSettingsService.ShowFilterHeader = true;
shellPage.ShellViewModel.FilesAndFoldersFilter += lowerCharString;
}
else
{
JumpString += lowerCharString;
}
}
// Handle special keys in filter mode
else if (isFilterModeOn && !string.IsNullOrEmpty(currentFilter))
{
switch (pressedKey)
{
case VirtualKey.Back when currentFilter.Length > 1:
shellPage.ShellViewModel.FilesAndFoldersFilter = currentFilter[..^1];
break;

case VirtualKey.Back when currentFilter.Length == 1:
shellPage.ShellViewModel.FilesAndFoldersFilter = string.Empty;
GeneralSettingsService.ShowFilterHeader = false;
break;
}
}

// Update selection in filter mode
if (isFilterModeOn)
{
char letter = args.Character;
JumpString += letter.ToString().ToLowerInvariant();
var filterText = shellPage.ShellViewModel.FilesAndFoldersFilter;
var matchedItem = shellPage.ShellViewModel.FilesAndFolders
.FirstOrDefault(item => !string.IsNullOrEmpty(filterText) &&
item.Name?.Contains(filterText, StringComparison.OrdinalIgnoreCase) == true);

if (matchedItem != null)
{
ItemManipulationModel.SetSelectedItem(matchedItem);
ItemManipulationModel.ScrollIntoView(matchedItem);
ItemManipulationModel.FocusSelectedItems();
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Files.App/Views/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ private async Task OnPreviewKeyDownAsync(KeyRoutedEventArgs e)
if (source?.FindAscendantOrSelf<TextBox>() is not null)
break;

// Prevent the Back and Space keys from executing a command if the keyboard
// typing behavior is set to filter items and a filter is currently applied.
if ((e.Key is VirtualKey.Back or VirtualKey.Space) &&
UserSettingsService.FoldersSettingsService.KeyboardTypingBehavior == KeyboardTypingBehavior.FilterItems &&
!string.IsNullOrEmpty(SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn!.ShellViewModel.FilesAndFoldersFilter))
break;

// Execute command for hotkey
var command = Commands[hotKey];

Expand Down
13 changes: 13 additions & 0 deletions src/Files.App/Views/Settings/AdvancedPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
xmlns:helpers="using:Files.App.Helpers"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:Files.App.UserControls"
xmlns:vm="using:Files.App.ViewModels.Settings"
xmlns:wctcontrols="using:CommunityToolkit.WinUI.Controls"
mc:Ignorable="d">
Expand Down Expand Up @@ -161,6 +162,18 @@
</ToggleSwitch>
</wctcontrols:SettingsCard>

<!-- Keyboard typing behavior -->
<wctcontrols:SettingsCard Header="{helpers:ResourceString Name=KeyboardTypingBehavior}">
<wctcontrols:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE765;" />
</wctcontrols:SettingsCard.HeaderIcon>

<uc:ComboBoxEx
AutomationProperties.Name="{helpers:ResourceString Name=KeyboardTypingBehavior}"
ItemsSource="{x:Bind ViewModel.KeyboardTypingBehaviors.Values}"
SelectedItem="{x:Bind ViewModel.KeyboardTypingBehavior, Mode=TwoWay}" />
</wctcontrols:SettingsCard>

<!-- Flatten options -->
<wctcontrols:SettingsCard Description="{helpers:ResourceString Name=ShowFlattenOptionsDescription}" Header="{helpers:ResourceString Name=ShowFlattenOptions}">
<wctcontrols:SettingsCard.HeaderIcon>
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Views/Shells/ModernShellPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
VerticalAlignment="Center"
PlaceholderText="{helpers:ResourceString Name=Filename}"
PreviewKeyDown="FilterTextBox_PreviewKeyDown"
TextChanged="FilterTextBox_TextChanged" />
Text="{x:Bind ShellViewModel.FilesAndFoldersFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>

Expand Down
Loading