diff --git a/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs b/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs
index 965776269510..6835bdf450ba 100644
--- a/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs
+++ b/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs
@@ -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;
}
diff --git a/src/Files.App/Data/Contracts/IFoldersSettingsService.cs b/src/Files.App/Data/Contracts/IFoldersSettingsService.cs
index f916ed3cbb2c..67e9c77d47fd 100644
--- a/src/Files.App/Data/Contracts/IFoldersSettingsService.cs
+++ b/src/Files.App/Data/Contracts/IFoldersSettingsService.cs
@@ -89,5 +89,10 @@ public interface IFoldersSettingsService : IBaseSettingsService, INotifyProperty
/// Gets or sets a value indicating which format to use when displaying item sizes.
///
SizeUnitTypes SizeUnitFormat { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the keyboard typing behavior.
+ ///
+ KeyboardTypingBehavior KeyboardTypingBehavior { get; set; }
}
}
diff --git a/src/Files.App/Data/Enums/KeyboardTypingBehavior.cs b/src/Files.App/Data/Enums/KeyboardTypingBehavior.cs
new file mode 100644
index 000000000000..05718ae47021
--- /dev/null
+++ b/src/Files.App/Data/Enums/KeyboardTypingBehavior.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+namespace Files.App.Data.Enums
+{
+ public enum KeyboardTypingBehavior
+ {
+ ///
+ /// Jump to matching item.
+ ///
+ JumpToFile,
+
+ ///
+ /// Filter items.
+ ///
+ FilterItems
+ }
+}
diff --git a/src/Files.App/Services/Settings/FoldersSettingsService.cs b/src/Files.App/Services/Settings/FoldersSettingsService.cs
index 45c16b0bcd31..656bc1470d8c 100644
--- a/src/Files.App/Services/Settings/FoldersSettingsService.cs
+++ b/src/Files.App/Services/Settings/FoldersSettingsService.cs
@@ -114,6 +114,13 @@ public SizeUnitTypes SizeUnitFormat
set => Set(value);
}
+ ///
+ 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);
diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index dc6dc947fbbf..c8e9002bb1a9 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -4278,6 +4278,15 @@
Filename
+
+
+ Behavior when typing in the file area
+
+
+ Jump to file
+
+
+ Filter items
Signatures
diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
index 6183a1330b78..bf87ef03af4b 100644
--- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
+++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
@@ -34,8 +34,8 @@ private static char[] RestrictedCharacters
{
var userSettingsService = Ioc.Default.GetRequiredService();
return userSettingsService.FoldersSettingsService.AreAlternateStreamsVisible
- ? ['\\', '/', '*', '?', '"', '<', '>', '|'] // Allow ":" char
- : ['\\', '/', ':', '*', '?', '"', '<', '>', '|'];
+ ? Path.GetInvalidFileNameChars().Where(c => c != ':').ToArray() // Allow ":" char when alternate streams are visible
+ : Path.GetInvalidFileNameChars();
}
}
diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs
index 2d8344bffaf2..0e9dd06ae541 100644
--- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs
+++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs
@@ -28,6 +28,7 @@ public sealed partial class AdvancedViewModel : ObservableObject
public ICommand ImportSettingsCommand { get; }
public AsyncRelayCommand OpenFilesOnWindowsStartupCommand { get; }
+ public Dictionary KeyboardTypingBehaviors { get; private set; } = [];
public AdvancedViewModel()
{
@@ -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();
}
@@ -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();
diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs
index bab020065ce7..ebdace09169c 100644
--- a/src/Files.App/ViewModels/ShellViewModel.cs
+++ b/src/Files.App/ViewModels/ShellViewModel.cs
@@ -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();
}
+ ///
+ /// Clears the files and folder filter.
+ /// This is used when the directory is changed or refreshed.
+ ///
+ 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()
@@ -1894,7 +1927,6 @@ await Task.Run(async () =>
});
filesAndFolders.AddRange(fileList);
- FilesAndFoldersFilter = null;
await OrderFilesAndFoldersAsync();
await ApplyFilesAndFoldersChangesAsync();
@@ -1903,6 +1935,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
{
GetDesktopIniFileData();
CheckForBackgroundImage();
+ ClearFilesAndFolderFilter();
},
Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
});
diff --git a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs
index 2d867c252741..65d88d704272 100644
--- a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs
+++ b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs
@@ -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() ||
@@ -112,7 +112,7 @@ focusedElement is PasswordBox ||
DependencyObjectHelpers.FindParent(focusedElement) is not null)
return;
- base.Page_CharacterReceived(sender, args);
+ base.Page_PreviewKeyDown(sender, e);
}
// Virtual methods
diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
index 277bd8df7815..addd47f6b267 100644
--- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs
+++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
@@ -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;
@@ -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;
@@ -40,6 +42,8 @@ public abstract class BaseLayoutPage : Page, IBaseLayoutPage, INotifyPropertyCha
protected IFileTagsSettingsService FileTagsSettingsService { get; } = Ioc.Default.GetService()!;
protected IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetService()!;
protected ILayoutSettingsService LayoutSettingsService { get; } = Ioc.Default.GetService()!;
+ protected IGeneralSettingsService GeneralSettingsService { get; } = Ioc.Default.GetService()!;
+ protected IFoldersSettingsService FoldersSettingsService { get; } = Ioc.Default.GetService()!;
protected ICommandManager Commands { get; } = Ioc.Default.GetRequiredService();
public InfoPaneViewModel InfoPaneViewModel { get; } = Ioc.Default.GetRequiredService();
protected readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService();
@@ -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;
@@ -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;
@@ -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)
+ {
+ var character = buffer[^1];
+ 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();
+ }
}
}
diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs
index 6f694cad775e..7596c6128342 100644
--- a/src/Files.App/Views/MainPage.xaml.cs
+++ b/src/Files.App/Views/MainPage.xaml.cs
@@ -216,6 +216,13 @@ private async Task OnPreviewKeyDownAsync(KeyRoutedEventArgs e)
if (source?.FindAscendantOrSelf() 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];
diff --git a/src/Files.App/Views/Settings/AdvancedPage.xaml b/src/Files.App/Views/Settings/AdvancedPage.xaml
index 4a001c502182..d55051c8e5f3 100644
--- a/src/Files.App/Views/Settings/AdvancedPage.xaml
+++ b/src/Files.App/Views/Settings/AdvancedPage.xaml
@@ -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">
@@ -161,6 +162,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Files.App/Views/Shells/ModernShellPage.xaml b/src/Files.App/Views/Shells/ModernShellPage.xaml
index cfd2f0c1564c..afbb5ad7b82c 100644
--- a/src/Files.App/Views/Shells/ModernShellPage.xaml
+++ b/src/Files.App/Views/Shells/ModernShellPage.xaml
@@ -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}" />
diff --git a/src/Files.App/Views/Shells/ModernShellPage.xaml.cs b/src/Files.App/Views/Shells/ModernShellPage.xaml.cs
index 98282ca8764d..dfad042c919a 100644
--- a/src/Files.App/Views/Shells/ModernShellPage.xaml.cs
+++ b/src/Files.App/Views/Shells/ModernShellPage.xaml.cs
@@ -45,7 +45,6 @@ public ModernShellPage() : base(new CurrentInstanceViewModel())
ShellViewModel.PageTypeUpdated += FilesystemViewModel_PageTypeUpdated;
ShellViewModel.OnSelectionRequestedEvent += FilesystemViewModel_OnSelectionRequestedEvent;
ShellViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated;
- ShellViewModel.DirectoryInfoUpdated += ShellViewModel_DirectoryInfoUpdated;
ShellViewModel.FocusFilterHeader += ShellViewModel_FocusFilterHeader;
ToolbarViewModel.PathControlDisplayText = Strings.Home.GetLocalizedResource();
@@ -63,13 +62,6 @@ private async void ShellViewModel_FocusFilterHeader(object sender, EventArgs e)
FilterTextBox.Focus(FocusState.Programmatic);
}
- private void ShellViewModel_DirectoryInfoUpdated(object sender, EventArgs e)
- {
- // Regular binding causes issues when refreshing the directory so we set the text manually
- if (FilterTextBox?.IsLoaded ?? false)
- FilterTextBox.Text = ShellViewModel.FilesAndFoldersFilter ?? string.Empty;
- }
-
private void ModernShellPage_RefreshWidgetsRequested(object sender, EventArgs e)
{
if (ItemDisplayFrame?.Content is HomePage currentPage)
@@ -297,8 +289,6 @@ public override void NavigateToReleaseNotes()
public override void NavigateToPath(string? navigationPath, Type? sourcePageType, NavigationArguments? navArgs = null)
{
- ShellViewModel.FilesAndFoldersFilter = null;
-
if (sourcePageType is null && !string.IsNullOrEmpty(navigationPath))
sourcePageType = InstanceViewModel.FolderSettings.GetLayoutType(navigationPath);
@@ -342,16 +332,6 @@ public override void NavigateToPath(string? navigationPath, Type? sourcePageType
ToolbarViewModel.PathControlDisplayText = ShellViewModel.WorkingDirectory;
}
- private async void FilterTextBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
- {
- if (args.Reason is AutoSuggestionBoxTextChangeReason.UserInput)
- {
- ShellViewModel.FilesAndFoldersFilter = sender.Text;
- await ShellViewModel.ApplyFilesAndFoldersChangesAsync();
- }
-
- }
-
private void FilterTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key is VirtualKey.Escape &&