diff --git a/src/Files.App/Actions/FileSystem/RenameAction.cs b/src/Files.App/Actions/FileSystem/RenameAction.cs index 621b1266668b..03071de00d20 100644 --- a/src/Files.App/Actions/FileSystem/RenameAction.cs +++ b/src/Files.App/Actions/FileSystem/RenameAction.cs @@ -33,15 +33,15 @@ public RenameAction() } public Task ExecuteAsync(object? parameter = null) - { + { + context.ShellPage?.SlimContentPage?.ItemManipulationModel.StartRenameItem(); - return Task.CompletedTask; } private bool IsSelectionValid() { - return context.HasSelection && context.SelectedItems.Count == 1; + return context.HasSelection && context.SelectedItems.Count != 0; } private bool IsPageTypeValid() diff --git a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs index 0742b61999ae..84b4b561fcaa 100644 --- a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs +++ b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs @@ -271,6 +271,72 @@ public static async Task RenameFileItemAsync(ListedItem item, string newNa return false; } + public static async Task RenameFileItemsAsync(List items, string newName, IShellPage associatedInstance, bool showExtensionDialog = true) + { + var tasks = items.Select(async (item, i) => + { + + + if (item is AlternateStreamItem ads) // For alternate streams Name is not a substring ItemNameRaw + { + newName = item.ItemNameRaw.Replace( + item.Name.Substring(item.Name.LastIndexOf(':') + 1), + newName.Substring(newName.LastIndexOf(':') + 1), + StringComparison.Ordinal); + newName = $"{ads.MainStreamName}:{newName}"; + } + else if (string.IsNullOrEmpty(item.Name)) + { + newName = string.Concat(newName, item.FileExtension); + } + else + { + + int new_name_extension = newName.LastIndexOf('.'); + if (new_name_extension != -1) + { + string newName_no_extension = newName.Substring(0, new_name_extension); + newName = string.Concat(newName_no_extension, item.FileExtension); + + } + else + { + newName = string.Concat(newName, item.FileExtension); + } + + newName = item.ItemNameRaw.Replace(item.Name, newName, StringComparison.Ordinal); + + + } + + if (item.ItemNameRaw == newName || string.IsNullOrEmpty(newName)) + { + return true; + } + + FilesystemItemType itemType = (item.PrimaryItemAttribute == StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File; + + ReturnResult renamed = await associatedInstance.FilesystemHelpers.RenameAsync(StorageHelpers.FromPathAndType(item.ItemPath, itemType), newName, NameCollisionOption.FailIfExists, true, showExtensionDialog); + + if (renamed == ReturnResult.Success) + { + associatedInstance.ToolbarViewModel.CanGoForward = false; + await associatedInstance.RefreshIfNoWatcherExistsAsync(); + return true; + + } + else + { + return false; + } + }); + + var results = await Task.WhenAll(tasks); + return results.All(x => x); + + + } + public static async Task CreateFileFromDialogResultTypeAsync(AddItemDialogItemType itemType, ShellNewEntry? itemInfo, IShellPage associatedInstance) { await CreateFileFromDialogResultTypeForResult(itemType, itemInfo, associatedInstance); diff --git a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs index 8e93fd76628f..d4ae2e87c08c 100644 --- a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs @@ -34,6 +34,7 @@ public abstract class BaseGroupableLayoutPage : BaseLayoutPage protected override ItemsControl ItemsControl => ListViewBase; + // Constructor public BaseGroupableLayoutPage() : base() @@ -227,7 +228,17 @@ protected virtual void SelectionRectangle_SelectionEnded(object? sender, EventAr protected virtual void StartRenameItem(string itemNameTextBox) { - RenamingItem = SelectedItem; + bool multipleRenameFlag = false; + if (SelectedItems.Count > 1) + { + RenamingItem = SelectedItems.Last(); + RenamingItems = SelectedItems; + multipleRenameFlag = true; + } + else + { + RenamingItem = SelectedItem; + } if (RenamingItem is null) return; @@ -264,15 +275,35 @@ protected virtual void StartRenameItem(string itemNameTextBox) selectedTextLength -= extensionLength; textBox.Select(0, selectedTextLength); - IsRenamingItem = true; + if(multipleRenameFlag) + { + IsRenamingMultipleItems = true; + } + else + { + IsRenamingItem = true; + } + } + + protected virtual async Task CommitRenameAsync(TextBox textBox) { EndRename(textBox); string newItemName = textBox.Text.Trim().TrimEnd('.'); + if (!IsRenamingMultipleItems) + { - await UIFilesystemHelpers.RenameFileItemAsync(RenamingItem, newItemName, ParentShellPageInstance); + await UIFilesystemHelpers.RenameFileItemAsync(RenamingItem, newItemName, ParentShellPageInstance); + IsRenamingItem = false; + } + else + { + + await UIFilesystemHelpers.RenameFileItemsAsync(RenamingItems, newItemName, ParentShellPageInstance); + IsRenamingMultipleItems = false; + } } protected virtual async void RenameTextBox_LostFocus(object sender, RoutedEventArgs e) @@ -281,6 +312,7 @@ protected virtual async void RenameTextBox_LostFocus(object sender, RoutedEventA if (!(FocusManager.GetFocusedElement(MainWindow.Instance.Content.XamlRoot) is AppBarButton or Popup)) { TextBox textBox = (TextBox)e.OriginalSource; + await CommitRenameAsync(textBox); } } @@ -289,6 +321,8 @@ protected virtual async void RenameTextBox_LostFocus(object sender, RoutedEventA protected async void RenameTextBox_KeyDown(object sender, KeyRoutedEventArgs e) { + Console.WriteLine($"Key pressed: {e.Key}"); + Console.WriteLine($"EnterKey : {VirtualKey.Enter}"); var textBox = (TextBox)sender; var isShiftPressed = (PInvoke.GetKeyState((int)VirtualKey.Shift) & KEY_DOWN_MASK) != 0; @@ -328,7 +362,9 @@ protected async void RenameTextBox_KeyDown(object sender, KeyRoutedEventArgs e) if (textBox.Text != OldItemName) { + await CommitRenameAsync(textBox); + } else { @@ -351,6 +387,7 @@ protected async void RenameTextBox_KeyDown(object sender, KeyRoutedEventArgs e) protected bool TryStartRenameNextItem(ListedItem item) { + var nextItemIndex = ListViewBase.Items.IndexOf(item) + NextRenameIndex; NextRenameIndex = 0; @@ -364,6 +401,8 @@ protected bool TryStartRenameNextItem(ListedItem item) } return false; + + } protected void SelectionCheckbox_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index 71c993679f35..18f5a4d83733 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -106,9 +106,15 @@ public bool AllowItemDrag public IShellPage? ParentShellPageInstance { get; private set; } public bool IsRenamingItem { get; set; } + + public bool IsRenamingMultipleItems { get; set; } + public bool LockPreviewPaneContent { get; set; } public ListedItem? RenamingItem { get; set; } + + public List? RenamingItems { get; set; } + public ListedItem? SelectedItem { get; private set; } public string? OldItemName { get; set; } diff --git a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs index 145334ba2356..48295448c0fe 100644 --- a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs @@ -124,10 +124,13 @@ protected override void ItemManipulationModel_FocusSelectedItemsInvoked(object? protected override void ItemManipulationModel_AddSelectedItemInvoked(object? sender, ListedItem e) { - if (NextRenameIndex != 0 && TryStartRenameNextItem(e)) - return; + if (!IsRenamingMultipleItems) + { + if (NextRenameIndex != 0 && TryStartRenameNextItem(e)) + return; - FileList?.SelectedItems.Add(e); + FileList?.SelectedItems.Add(e); + } } protected override void ItemManipulationModel_RemoveSelectedItemInvoked(object? sender, ListedItem e) @@ -198,7 +201,7 @@ override public void StartRenameItem() private async void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBeforeTextChangingEventArgs args) { - if (IsRenamingItem) + if (IsRenamingItem || IsRenamingMultipleItems) { await ValidateItemNameInputTextAsync(textBox, args, (showError) => { @@ -211,9 +214,7 @@ private async void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBe protected override void EndRename(TextBox textBox) { FileNameTeachingTip.IsOpen = false; - IsRenamingItem = false; - - // Unsubscribe from events + if (textBox is not null) { textBox!.LostFocus -= RenameTextBox_LostFocus; @@ -222,7 +223,8 @@ protected override void EndRename(TextBox textBox) if (textBox is not null && textBox.Parent is not null) { - ListViewItem? listViewItem = FileList.ContainerFromItem(RenamingItem) as ListViewItem; + ListViewItem? listViewItem; + listViewItem = FileList.ContainerFromItem(RenamingItem) as ListViewItem; if (listViewItem is null) return; @@ -233,6 +235,7 @@ protected override void EndRename(TextBox textBox) textBox!.Visibility = Visibility.Collapsed; textBlock!.Visibility = Visibility.Visible; } + } public override void ResetItemOpacity() diff --git a/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs index 625deb606b17..140819bb8a67 100644 --- a/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs @@ -328,21 +328,14 @@ private void FileList_SelectionChanged(object sender, SelectionChangedEventArgs override public void StartRenameItem() { + StartRenameItem("ItemNameTextBox"); - if (FileList.ContainerFromItem(RenamingItem) is not ListViewItem listViewItem) - return; - - var textBox = listViewItem.FindDescendant("ItemNameTextBox") as TextBox; - if (textBox is null || textBox.FindParent() is null) - return; - - Grid.SetColumnSpan(textBox.FindParent(), 8); } private void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBeforeTextChangingEventArgs args) { - if (IsRenamingItem) + if (IsRenamingItem || IsRenamingMultipleItems) { ValidateItemNameInputTextAsync(textBox, args, (showError) => { @@ -354,10 +347,27 @@ private void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBeforeTe protected override void EndRename(TextBox textBox) { + if (textBox is not null) + { + textBox.LostFocus -= RenameTextBox_LostFocus; + textBox.KeyDown -= RenameTextBox_KeyDown; + } if (textBox is not null && textBox.FindParent() is FrameworkElement parent) Grid.SetColumnSpan(parent, 1); - ListViewItem? listViewItem = FileList.ContainerFromItem(RenamingItem) as ListViewItem; + ListViewItem? listViewItem; + + + listViewItem = FileList.ContainerFromItem(RenamingItem) as ListViewItem; + + + if (listViewItem is null) + { + IsRenamingMultipleItems = false; + IsRenamingItem = false; + return; + } + if (textBox is null || listViewItem is null) { @@ -365,23 +375,20 @@ protected override void EndRename(TextBox textBox) } else { + listViewItem?.Focus(FocusState.Programmatic); TextBlock? textBlock = listViewItem.FindDescendant("ItemName") as TextBlock; textBox.Visibility = Visibility.Collapsed; textBlock!.Visibility = Visibility.Visible; } // Unsubscribe from events - if (textBox is not null) - { - textBox!.LostFocus -= RenameTextBox_LostFocus; - textBox.KeyDown -= RenameTextBox_KeyDown; - } + FileNameTeachingTip.IsOpen = false; - IsRenamingItem = false; + // Re-focus selected list item - listViewItem?.Focus(FocusState.Programmatic); + } protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEventArgs e) @@ -404,7 +411,7 @@ protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEv await commands[hotKey].ExecuteAsync(); } - else if (e.Key == VirtualKey.Enter && !e.KeyStatus.IsMenuKeyDown) + else if (e.Key == VirtualKey.Enter && !e.KeyStatus.IsMenuKeyDown && !IsRenamingMultipleItems) { e.Handled = true; @@ -429,7 +436,7 @@ protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEv FilePropertiesHelpers.OpenPropertiesWindow(ParentShellPageInstance); e.Handled = true; } - else if (e.Key == VirtualKey.Space) + else if (e.Key == VirtualKey.Space && !IsRenamingMultipleItems) { if (!ParentShellPageInstance.ToolbarViewModel.IsEditModeEnabled) e.Handled = true; @@ -501,7 +508,7 @@ private async void FileList_ItemTapped(object sender, TappedRoutedEventArgs e) var item = clickedItem?.DataContext as ListedItem; if (item is null) { - if (IsRenamingItem && RenamingItem is not null) + if ((IsRenamingItem && RenamingItem is not null) || (IsRenamingMultipleItems && RenamingItem is not null)) { ListViewItem? listViewItem = FileList.ContainerFromItem(RenamingItem) as ListViewItem; if (listViewItem is not null) @@ -511,6 +518,7 @@ private async void FileList_ItemTapped(object sender, TappedRoutedEventArgs e) await CommitRenameAsync(textBox); } } + return; } diff --git a/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs index 6455b3bfb919..34c0e03164c3 100644 --- a/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs @@ -285,7 +285,18 @@ protected override void FileList_SelectionChanged(object sender, SelectionChange override public void StartRenameItem() { - RenamingItem = SelectedItem; + bool multipleRenameFlag = false; + if (SelectedItems.Count > 1) + { + RenamingItem = SelectedItems.Last(); + RenamingItems = SelectedItems; + multipleRenameFlag = true; + } + else + { + RenamingItem = SelectedItem; + } + if (RenamingItem is null || FolderSettings is null) return; @@ -360,12 +371,20 @@ override public void StartRenameItem() selectedTextLength -= extensionLength; textBox.Select(0, selectedTextLength); - IsRenamingItem = true; + if(multipleRenameFlag) + { + IsRenamingMultipleItems = true; + } + else + { + IsRenamingItem = true; + } + } private void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBeforeTextChangingEventArgs args) { - if (!IsRenamingItem) + if (!IsRenamingItem && !IsRenamingMultipleItems) return; ValidateItemNameInputTextAsync(textBox, args, (showError) => @@ -412,7 +431,7 @@ protected override void EndRename(TextBox textBox) } FileNameTeachingTip.IsOpen = false; - IsRenamingItem = false; + // Re-focus selected list item gridViewItem?.Focus(FocusState.Programmatic); @@ -437,7 +456,7 @@ protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEv await commands[hotKey].ExecuteAsync(); } - else if (e.Key == VirtualKey.Enter && !isFooterFocused && !e.KeyStatus.IsMenuKeyDown) + else if (e.Key == VirtualKey.Enter && !isFooterFocused && !e.KeyStatus.IsMenuKeyDown && !IsRenamingMultipleItems) { e.Handled = true; @@ -460,7 +479,7 @@ protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEv FilePropertiesHelpers.OpenPropertiesWindow(ParentShellPageInstance); e.Handled = true; } - else if (e.Key == VirtualKey.Space) + else if (e.Key == VirtualKey.Space && !IsRenamingMultipleItems) { if (!ParentShellPageInstance.ToolbarViewModel.IsEditModeEnabled) e.Handled = true; diff --git a/src/Files.App/Views/Layouts/IBaseLayoutPage.cs b/src/Files.App/Views/Layouts/IBaseLayoutPage.cs index 2e2a8bea9875..0cfcf4ef15e4 100644 --- a/src/Files.App/Views/Layouts/IBaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/IBaseLayoutPage.cs @@ -10,6 +10,8 @@ public interface IBaseLayoutPage : IDisposable { bool IsRenamingItem { get; } + bool IsRenamingMultipleItems { get; } + bool IsItemSelected { get; } bool IsMiddleClickToScrollEnabled { get; set; } diff --git a/src/Files.App/Views/Properties/GeneralPage.xaml.cs b/src/Files.App/Views/Properties/GeneralPage.xaml.cs index 1238d99e5271..f1dd586423c5 100644 --- a/src/Files.App/Views/Properties/GeneralPage.xaml.cs +++ b/src/Files.App/Views/Properties/GeneralPage.xaml.cs @@ -150,6 +150,12 @@ async Task SaveCombinedAsync(IList fileOrFolders) AppInstance?.FilesystemViewModel?.RefreshItems(null); }); } + if (!GetNewName(out var newName)) + continue; + + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + UIFilesystemHelpers.RenameFileItemAsync(fileOrFolder, ViewModel.ItemName, AppInstance, false) + ); } } return true; diff --git a/tests/Files.InteractionTests/Tests/FolderTests.cs b/tests/Files.InteractionTests/Tests/FolderTests.cs index 7bcf37afdaca..a6492660c10a 100644 --- a/tests/Files.InteractionTests/Tests/FolderTests.cs +++ b/tests/Files.InteractionTests/Tests/FolderTests.cs @@ -29,6 +29,8 @@ public void TestFolders() CopyPasteFolderTest(); + RenameMultipleFoldersTest(); + DeleteFolderTest(); } @@ -118,28 +120,58 @@ private void CopyPasteFolderTest() Thread.Sleep(3000); } + private void RenameMultipleFoldersTest() + { + // Select the "New Folder" folder and clicks the "rename" button on the toolbar + TestHelper.InvokeButtonByName("Renamed Folder"); + + // Press the ctrl button + var action = new Actions(SessionManager.Session); + action.SendKeys(Keys.Control).Perform(); + + // Select the "New Folder - Copy" folder and clicks the "rename" button on the toolbar + TestHelper.InvokeButtonByName("Renamed Folder - Copy"); + + // Release the ctrl button + action = new Actions(SessionManager.Session); + action.KeyUp(Keys.Control).Perform(); + + // Click the "Rename" button on the toolbar + TestHelper.InvokeButtonById("InnerNavigationToolbarRenameButton"); + + // Type the new name into the inline text box + action = new Actions(SessionManager.Session); + action.SendKeys("Multiple Folder").Perform(); + + // Press the enter button to save the new name + action = new Actions(SessionManager.Session); + action.SendKeys(Keys.Enter).Perform(); + + // Wait for the folder to be renamed + Thread.Sleep(3000); + + } + /// /// Tests deleting folders /// private void DeleteFolderTest() { // Select the "Renamed Folder" folder and clicks the "delete" button on the toolbar - TestHelper.InvokeButtonByName("Renamed Folder"); - TestHelper.InvokeButtonById("InnerNavigationToolbarDeleteButton"); + TestHelper.InvokeButtonByName("Multiple Folder"); - // Wait for prompt to show - Thread.Sleep(3000); - - // Check for accessibility issues in the confirm delete prompt - AxeHelper.AssertNoAccessibilityErrors(); - - // Press the enter key to confirm + // Press the ctrl button var action = new Actions(SessionManager.Session); - action.SendKeys(Keys.Enter).Perform(); + action.SendKeys(Keys.Control).Perform(); + // Select the "Renamed Folder (2)" folder + TestHelper.InvokeButtonByName("Multiple Folder (2)"); - // Select the "Renamed Folder - Copy" folder and clicks the "delete" button on the toolbar - TestHelper.InvokeButtonByName("Renamed Folder - Copy"); + // Release the ctrl button + action = new Actions(SessionManager.Session); + action.KeyUp(Keys.Control).Perform(); + + // Click the "Delete" button on the toolbar TestHelper.InvokeButtonById("InnerNavigationToolbarDeleteButton"); // Wait for prompt to show @@ -156,4 +188,4 @@ private void DeleteFolderTest() Thread.Sleep(3000); } } -} +} \ No newline at end of file