diff --git a/specs/RichCommand/CommandList.md b/specs/RichCommand/CommandList.md index ec0f8957f210..fdd091b4cff0 100644 --- a/specs/RichCommand/CommandList.md +++ b/specs/RichCommand/CommandList.md @@ -35,7 +35,7 @@ This is the list of all commands defined in `CommandCodes` enum except `None`. | | RestoreRecycleBin | Restore | Restore selected item(s) from recycle bin | | | | RestoreAllRecycleBin | Restore All Items | Restore all items from recycle bin | | | | OpenItem | Open | Open item(s) | Enter | -| | OpenItemWithApplicationPicker | Open With | Open item(s) with selected application | | +| | OpenItemWithApplicationPicker | Open with | Open item(s) with selected application | | | | OpenParentFolder | Open parent folder | Open parent folder of searched item | | | | OpenFileLocation | Open file location | Open the item's location | | | | RefreshItems | Refresh | Refresh page contents | Ctrl+R, F5 | @@ -73,6 +73,7 @@ This is the list of all commands defined in `CommandCodes` enum except `None`. | | OpenSettings | Settings | Open settings page | Ctrl+, | | | OpenTerminal | Open in terminal | Open folder in terminal | Ctrl+\` | | | OpenTerminalAsAdmin | Open in terminal as administrator | Open folder in terminal as administrator | Ctrl+Shift+\` | +| | OpenCommandPalette | Command palette | Open command palette | Ctrl+Shift+P | | Layout | LayoutDecreaseSize | Decrease size | Decrease icon size in grid view | Ctrl+- | | | LayoutIncreaseSize | Increase size | Increase icon size in grid view | Ctrl++ | | | LayoutDetails | Details | Switch to details view | Ctrl+Shift+1 | @@ -131,6 +132,9 @@ This is the list of all commands defined in `CommandCodes` enum except `None`. | | CloseTabsToTheRightSelected | Close tabs to the right | Close tabs to the right of selected tab | | | | CloseOtherTabsCurrent | Close other tabs | Close tabs other than current tab | | | | CloseOtherTabsSelected | Close other tabs | Close tabs other than selected tab | | +| | OpenDirectoryInNewPane | Open in new pane | Open directory in new pane | | +| | OpenDirectoryInNewTab | Open in new tab | Open directory in new tab | | +| | OpenInNewWindowItem | Open in new window | Open directory in new window | | | | ReopenClosedTab | Reopen closed tab | Reopen last closed tab | Ctrl+Shift+T | | | PreviousTab | Moves to the previous tab | Move to the previous tab | Ctrl+Shift+Tab | | | NextTab | Moves to the next tab | Move to the next tab | Ctrl+Tab | @@ -143,3 +147,4 @@ This is the list of all commands defined in `CommandCodes` enum except `None`. | | GitPull | Pull | Run git pull | | | | GitPush | Push | Run git push | | | | GitSync | Sync | Run git pull and then git push | | +| Tags | OpenAllTaggedItems | Open all | Open all tagged items | | diff --git a/src/Files.App/Actions/Open/OpenCommandPaletteAction.cs b/src/Files.App/Actions/Open/OpenCommandPaletteAction.cs new file mode 100644 index 000000000000..87e2de84307d --- /dev/null +++ b/src/Files.App/Actions/Open/OpenCommandPaletteAction.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Actions +{ + internal class OpenCommandPaletteAction : IAction + { + private readonly IContentPageContext _context; + + public string Label + => "CommandPalette".GetLocalizedResource(); + + public string Description + => "OpenCommandPaletteDescription".GetLocalizedResource(); + + public HotKey HotKey + => new(Keys.P, KeyModifiers.CtrlShift); + + public OpenCommandPaletteAction() + { + _context = Ioc.Default.GetRequiredService(); + } + + public Task ExecuteAsync() + { + _context.ShellPage?.ToolbarViewModel.OpenCommandPalette(); + + return Task.CompletedTask; + } + } +} diff --git a/src/Files.App/Data/Commands/CommandCodes.cs b/src/Files.App/Data/Commands/CommandCodes.cs index 34dfb695b6a6..61e1cf21d99b 100644 --- a/src/Files.App/Data/Commands/CommandCodes.cs +++ b/src/Files.App/Data/Commands/CommandCodes.cs @@ -103,6 +103,7 @@ public enum CommandCodes OpenSettings, OpenTerminal, OpenTerminalAsAdmin, + OpenCommandPalette, // Layout LayoutDecreaseSize, diff --git a/src/Files.App/Data/Commands/Manager/CommandManager.cs b/src/Files.App/Data/Commands/Manager/CommandManager.cs index 9c92bb821104..c9a0cc73e873 100644 --- a/src/Files.App/Data/Commands/Manager/CommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/CommandManager.cs @@ -18,6 +18,20 @@ internal class CommandManager : ICommandManager private IImmutableDictionary hotKeys = new Dictionary().ToImmutableDictionary(); public IRichCommand this[CommandCodes code] => commands.TryGetValue(code, out var command) ? command : None; + public IRichCommand this[string code] + { + get + { + try + { + return commands[Enum.Parse(code, true)]; + } + catch + { + return None; + } + } + } public IRichCommand this[HotKey hotKey] => hotKeys.TryGetValue(hotKey with { IsVisible = true }, out var command) ? command : hotKeys.TryGetValue(hotKey with { IsVisible = false }, out command) ? command @@ -91,6 +105,7 @@ public IRichCommand this[HotKey hotKey] public IRichCommand OpenSettings => commands[CommandCodes.OpenSettings]; public IRichCommand OpenTerminal => commands[CommandCodes.OpenTerminal]; public IRichCommand OpenTerminalAsAdmin => commands[CommandCodes.OpenTerminalAsAdmin]; + public IRichCommand OpenCommandPalette => commands[CommandCodes.OpenCommandPalette]; public IRichCommand LayoutDecreaseSize => commands[CommandCodes.LayoutDecreaseSize]; public IRichCommand LayoutIncreaseSize => commands[CommandCodes.LayoutIncreaseSize]; public IRichCommand LayoutDetails => commands[CommandCodes.LayoutDetails]; @@ -252,6 +267,7 @@ public CommandManager() [CommandCodes.OpenSettings] = new OpenSettingsAction(), [CommandCodes.OpenTerminal] = new OpenTerminalAction(), [CommandCodes.OpenTerminalAsAdmin] = new OpenTerminalAsAdminAction(), + [CommandCodes.OpenCommandPalette] = new OpenCommandPaletteAction(), [CommandCodes.LayoutDecreaseSize] = new LayoutDecreaseSizeAction(), [CommandCodes.LayoutIncreaseSize] = new LayoutIncreaseSizeAction(), [CommandCodes.LayoutDetails] = new LayoutDetailsAction(), diff --git a/src/Files.App/Data/Commands/Manager/ICommandManager.cs b/src/Files.App/Data/Commands/Manager/ICommandManager.cs index ea9840303a60..142952cda7a5 100644 --- a/src/Files.App/Data/Commands/Manager/ICommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/ICommandManager.cs @@ -6,6 +6,7 @@ namespace Files.App.Data.Commands public interface ICommandManager : IEnumerable { IRichCommand this[CommandCodes code] { get; } + IRichCommand this[string code] { get; } IRichCommand this[HotKey customHotKey] { get; } IRichCommand None { get; } @@ -89,6 +90,7 @@ public interface ICommandManager : IEnumerable IRichCommand OpenSettings { get; } IRichCommand OpenTerminal { get; } IRichCommand OpenTerminalAsAdmin { get; } + IRichCommand OpenCommandPalette { get; } IRichCommand LayoutDecreaseSize { get; } IRichCommand LayoutIncreaseSize { get; } diff --git a/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs new file mode 100644 index 000000000000..2fc584ed4689 --- /dev/null +++ b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2023 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.UI.Xaml.Media; + +namespace Files.App.Data.Items +{ + public class NavigationBarSuggestionItem : ObservableObject + { + private string? _Text; + public string? Text + { + get => _Text; + set => SetProperty(ref _Text, value); + } + + private string? _PrimaryDisplay; + public string? PrimaryDisplay + { + get => _PrimaryDisplay; + set => SetProperty(ref _PrimaryDisplay, value); + } + + private string? _SecondaryDisplay; + public string? SecondaryDisplay + { + get => _SecondaryDisplay; + set => SetProperty(ref _SecondaryDisplay, value); + } + + private string? _SupplementaryDisplay; + public string? SupplementaryDisplay + { + get => _SupplementaryDisplay; + set => SetProperty(ref _SupplementaryDisplay, value); + } + } +} diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index fdbe0debfa82..4c00c7a4debb 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -175,7 +175,7 @@ Clear all items - Enter a path + Enter a path to navigate to or type ">" to open the command palette Search @@ -3404,4 +3404,22 @@ Open all tagged items + + Invalid command + + + '{0}' is not recognized as a command. + + + Command not executable + + + The '{0}' command is not ready to be executed. + + + Command palette + + + Open command palette + \ No newline at end of file diff --git a/src/Files.App/UserControls/AddressToolbar.xaml b/src/Files.App/UserControls/AddressToolbar.xaml index 0ee690ed935c..bfe153e11d50 100644 --- a/src/Files.App/UserControls/AddressToolbar.xaml +++ b/src/Files.App/UserControls/AddressToolbar.xaml @@ -11,6 +11,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:extensions="using:CommunityToolkit.WinUI.UI" xmlns:helpers="using:Files.App.Helpers" + xmlns:items="using:Files.App.Data.Items" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:triggers="using:CommunityToolkit.WinUI.UI.Triggers" xmlns:uc="using:Files.App.UserControls" @@ -23,6 +24,7 @@ + @@ -124,7 +126,6 @@ BorderBrush="{ThemeResource SystemBaseMediumLowColor}" BorderThickness="{ThemeResource TextControlBorderThemeThickness}" CornerRadius="{StaticResource ControlCornerRadius}" - DisplayMemberPath="Name" FocusDisengaged="VisiblePath_LostFocus" FontWeight="SemiBold" ItemsSource="{x:Bind ViewModel.NavigationBarSuggestions, Mode=OneWay}" @@ -137,8 +138,44 @@ ScrollViewer.VerticalScrollBarVisibility="Hidden" Text="{x:Bind ViewModel.PathText, Mode=OneWay}" TextChanged="{x:Bind ViewModel.VisiblePath_TextChanged, Mode=OneWay}" - TextMemberPath="ItemPath" - Visibility="{x:Bind converters:MultiBooleanConverter.OrNotConvertToVisibility(ShowSearchBox, ViewModel.IsSearchBoxVisible), Mode=OneWay}" /> + TextMemberPath="Text" + Visibility="{x:Bind converters:MultiBooleanConverter.OrNotConvertToVisibility(ShowSearchBox, ViewModel.IsSearchBoxVisible), Mode=OneWay}"> + + + + + + + + + + + + + + + + + + + + + + + + (VisiblePath)?.SelectAll(); + + if (DependencyObjectHelpers.FindChild(VisiblePath) is TextBox textBox) + { + if (textBox.Text.StartsWith(">")) + textBox.Select(1, textBox.Text.Length - 1); + else + textBox.SelectAll(); + } } private void ManualPathEntryItem_Click(object _, PointerRoutedEventArgs e) diff --git a/src/Files.App/ViewModels/UserControls/ToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/ToolbarViewModel.cs index 4d78dd1e22bc..9104f77ff9e5 100644 --- a/src/Files.App/ViewModels/UserControls/ToolbarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/ToolbarViewModel.cs @@ -7,6 +7,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using System.IO; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; @@ -165,7 +166,7 @@ public string? PathText } } - public ObservableCollection NavigationBarSuggestions = new(); + public ObservableCollection NavigationBarSuggestions = new(); private CurrentInstanceViewModel instanceViewModel; public CurrentInstanceViewModel InstanceViewModel @@ -508,6 +509,16 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => }); } + public void OpenCommandPalette() + { + PathText = ">"; + ManualEntryBoxLoaded = true; + ClickablePathLoaded = false; + + var visiblePath = AddressToolbar?.FindDescendant(x => x.Name == "VisiblePath"); + AddressBarTextEntered?.Invoke(this, new AddressBarTextEnteredEventArgs() { AddressBarTextField = visiblePath }); + } + public void SwitchSearchBoxVisibility() { if (IsSearchBoxVisible) @@ -644,6 +655,23 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p public async Task CheckPathInput(string currentInput, string currentSelectedPath, IShellPage shellPage) { + if (currentInput.StartsWith('>')) + { + var code = currentInput.Substring(1).Trim(); + var command = Commands[code]; + + if (command == Commands.None) + await DialogDisplayHelper.ShowDialogAsync("InvalidCommand".GetLocalizedResource(), + string.Format("InvalidCommandContent".GetLocalizedResource(), code)); + else if (!command.IsExecutable) + await DialogDisplayHelper.ShowDialogAsync("CommandNotExecutable".GetLocalizedResource(), + string.Format("CommandNotExecutableContent".GetLocalizedResource(), command.Code)); + else + await command.ExecuteAsync(); + + return; + } + var isFtp = FtpHelpers.IsFtpPath(currentInput); if (currentInput.Contains('/') && !isFtp) @@ -751,60 +779,80 @@ public async Task SetAddressBarSuggestions(AutoSuggestBox sender, IShellPage she { if (!await SafetyExtensions.IgnoreExceptions(async () => { - IList? suggestions = null; - var isFtp = FtpHelpers.IsFtpPath(sender.Text); - var expandedPath = StorageFileExtensions.GetResolvedPath(sender.Text, isFtp); - var folderPath = PathNormalization.GetParentDir(expandedPath) ?? expandedPath; - StorageFolderWithPath folder = await shellpage.FilesystemViewModel.GetFolderWithPathFromPathAsync(folderPath); + IList? suggestions = null; - if (folder is null) - return false; - - var currPath = await folder.GetFoldersWithPathAsync(Path.GetFileName(expandedPath), (uint)maxSuggestions); - if (currPath.Count >= maxSuggestions) + if (sender.Text.StartsWith(">")) { - suggestions = currPath.Select(x => new ListedItem(null!) + var searchText = sender.Text.Substring(1).Trim(); + suggestions = Commands.Where(command => command.IsExecutable && + (command.Description.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || command.Code.ToString().Contains(searchText, StringComparison.OrdinalIgnoreCase))) + .Select(command => new NavigationBarSuggestionItem() { - ItemPath = x.Path, - ItemNameRaw = x.Item.DisplayName + Text = ">" + command.Code, + PrimaryDisplay = command.Description, + SupplementaryDisplay = command.HotKeyText, }).ToList(); } - else if (currPath.Any()) + else { - var subPath = await currPath.First().GetFoldersWithPathAsync((uint)(maxSuggestions - currPath.Count)); - suggestions = currPath.Select(x => new ListedItem(null!) + var isFtp = FtpHelpers.IsFtpPath(sender.Text); + var expandedPath = StorageFileExtensions.GetResolvedPath(sender.Text, isFtp); + var folderPath = PathNormalization.GetParentDir(expandedPath) ?? expandedPath; + StorageFolderWithPath folder = await shellpage.FilesystemViewModel.GetFolderWithPathFromPathAsync(folderPath); + + if (folder is null) + return false; + + var currPath = await folder.GetFoldersWithPathAsync(Path.GetFileName(expandedPath), (uint)maxSuggestions); + if (currPath.Count >= maxSuggestions) + { + suggestions = currPath.Select(x => new NavigationBarSuggestionItem() + { + Text = x.Path, + PrimaryDisplay = x.Item.DisplayName + }).ToList(); + } + else if (currPath.Any()) { - ItemPath = x.Path, - ItemNameRaw = x.Item.DisplayName - }).Concat( - subPath.Select(x => new ListedItem(null!) + var subPath = await currPath.First().GetFoldersWithPathAsync((uint)(maxSuggestions - currPath.Count)); + suggestions = currPath.Select(x => new NavigationBarSuggestionItem() { - ItemPath = x.Path, - ItemNameRaw = PathNormalization.Combine(currPath.First().Item.DisplayName, x.Item.DisplayName) - })).ToList(); + Text = x.Path, + PrimaryDisplay = x.Item.DisplayName + }).Concat( + subPath.Select(x => new NavigationBarSuggestionItem() + { + Text = x.Path, + PrimaryDisplay = PathNormalization.Combine(currPath.First().Item.DisplayName, x.Item.DisplayName) + })).ToList(); + } } - else + + if (suggestions is null || suggestions.Count == 0) { - suggestions = new List() { new ListedItem(null!) { - ItemPath = shellpage.FilesystemViewModel.WorkingDirectory, - ItemNameRaw = "NavigationToolbarVisiblePathNoResults".GetLocalizedResource() } }; + suggestions = new List() { new NavigationBarSuggestionItem() { + Text = shellpage.FilesystemViewModel.WorkingDirectory, + PrimaryDisplay = "NavigationToolbarVisiblePathNoResults".GetLocalizedResource() } }; } // NavigationBarSuggestions becoming empty causes flickering of the suggestion box // Here we check whether at least an element is in common between old and new list - if (!NavigationBarSuggestions.IntersectBy(suggestions, x => x.Name).Any()) + if (!NavigationBarSuggestions.IntersectBy(suggestions, x => x.PrimaryDisplay).Any()) { // No elements in common, update the list in-place - for (int si = 0; si < suggestions.Count; si++) + for (int index = 0; index < suggestions.Count; index++) { - if (si < NavigationBarSuggestions.Count) + if (index < NavigationBarSuggestions.Count) { - NavigationBarSuggestions[si].ItemNameRaw = suggestions[si].ItemNameRaw; - NavigationBarSuggestions[si].ItemPath = suggestions[si].ItemPath; + NavigationBarSuggestions[index].Text = suggestions[index].Text; + NavigationBarSuggestions[index].PrimaryDisplay = suggestions[index].PrimaryDisplay; + NavigationBarSuggestions[index].SecondaryDisplay = suggestions[index].SecondaryDisplay; + NavigationBarSuggestions[index].SupplementaryDisplay = suggestions[index].SupplementaryDisplay; } else { - NavigationBarSuggestions.Add(suggestions[si]); + NavigationBarSuggestions.Add(suggestions[index]); } } @@ -814,10 +862,10 @@ public async Task SetAddressBarSuggestions(AutoSuggestBox sender, IShellPage she else { // At least an element in common, show animation - foreach (var s in NavigationBarSuggestions.ExceptBy(suggestions, x => x.ItemNameRaw).ToList()) + foreach (var s in NavigationBarSuggestions.ExceptBy(suggestions, x => x.PrimaryDisplay).ToList()) NavigationBarSuggestions.Remove(s); - foreach (var s in suggestions.ExceptBy(NavigationBarSuggestions, x => x.ItemNameRaw).ToList()) + foreach (var s in suggestions.ExceptBy(NavigationBarSuggestions, x => x.PrimaryDisplay).ToList()) NavigationBarSuggestions.Insert(suggestions.IndexOf(s), s); } @@ -825,10 +873,10 @@ public async Task SetAddressBarSuggestions(AutoSuggestBox sender, IShellPage she })) { NavigationBarSuggestions.Clear(); - NavigationBarSuggestions.Add(new ListedItem(null!) + NavigationBarSuggestions.Add(new NavigationBarSuggestionItem() { - ItemPath = shellpage.FilesystemViewModel.WorkingDirectory, - ItemNameRaw = "NavigationToolbarVisiblePathNoResults".GetLocalizedResource() + Text = shellpage.FilesystemViewModel.WorkingDirectory, + PrimaryDisplay = "NavigationToolbarVisiblePathNoResults".GetLocalizedResource() }); } }