From c93e9c023e655e821c74f89adad398aca4b893ee Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:54:34 +0900 Subject: [PATCH 1/3] Init --- .../Omnibar/Omnibar.Events.cs | 7 +- .../Omnibar/Omnibar.Properties.cs | 16 + src/Files.App.Controls/Omnibar/Omnibar.cs | 23 +- src/Files.App.Controls/Omnibar/Omnibar.xaml | 1 - .../Omnibar/OmnibarMode.Properties.cs | 13 +- .../Data/Items/NavigationBarSuggestionItem.cs | 1 + .../Data/Models/BreadcrumbBarItemModel.cs | 7 + .../Models/OmnibarPathModeSuggestionModel.cs | 20 ++ src/Files.App/Strings/en-US/Resources.resw | 3 + .../UserControls/NavigationToolbar.xaml | 122 +++++++- .../UserControls/NavigationToolbar.xaml.cs | 142 ++++++--- .../UserControls/PathBreadcrumb.xaml.cs | 10 +- .../NavigationToolbarViewModel.cs | 284 ++++++++++++++++-- src/Files.App/Views/Shells/BaseShellPage.cs | 7 +- 14 files changed, 559 insertions(+), 97 deletions(-) create mode 100644 src/Files.App/Data/Models/BreadcrumbBarItemModel.cs create mode 100644 src/Files.App/Data/Models/OmnibarPathModeSuggestionModel.cs diff --git a/src/Files.App.Controls/Omnibar/Omnibar.Events.cs b/src/Files.App.Controls/Omnibar/Omnibar.Events.cs index 06161def1f95..ebd4f5bcb457 100644 --- a/src/Files.App.Controls/Omnibar/Omnibar.Events.cs +++ b/src/Files.App.Controls/Omnibar/Omnibar.Events.cs @@ -16,7 +16,7 @@ private void Omnibar_SizeChanged(object sender, SizeChangedEventArgs e) private void AutoSuggestBox_GotFocus(object sender, RoutedEventArgs e) { - _isFocused = true; + IsFocused = true; VisualStateManager.GoToState(CurrentSelectedMode, "Focused", true); VisualStateManager.GoToState(_textBox, "InputAreaVisible", true); @@ -30,7 +30,7 @@ private void AutoSuggestBox_LostFocus(object sender, RoutedEventArgs e) if (_textBox.ContextFlyout.IsOpen) return; - _isFocused = false; + IsFocused = false; if (CurrentSelectedMode?.ContentOnInactive is not null) { @@ -92,7 +92,8 @@ private void AutoSuggestBox_KeyDown(object sender, KeyRoutedEventArgs e) private void AutoSuggestBox_TextChanged(object sender, TextChangedEventArgs e) { - CurrentSelectedMode!.Text = _textBox.Text; + if (string.Compare(_textBox.Text, CurrentSelectedMode!.Text, StringComparison.OrdinalIgnoreCase) is not 0) + CurrentSelectedMode!.Text = _textBox.Text; // UpdateSuggestionListView(); diff --git a/src/Files.App.Controls/Omnibar/Omnibar.Properties.cs b/src/Files.App.Controls/Omnibar/Omnibar.Properties.cs index e530e55e69fd..d4d8ff449cab 100644 --- a/src/Files.App.Controls/Omnibar/Omnibar.Properties.cs +++ b/src/Files.App.Controls/Omnibar/Omnibar.Properties.cs @@ -13,7 +13,23 @@ public partial class Omnibar [GeneratedDependencyProperty] public partial OmnibarMode? CurrentSelectedMode { get; set; } + [GeneratedDependencyProperty] + public partial string? CurrentSelectedModeName { get; set; } + [GeneratedDependencyProperty] public partial Thickness AutoSuggestBoxPadding { get; set; } + + [GeneratedDependencyProperty] + public partial bool IsFocused { get; set; } + + partial void OnCurrentSelectedModeChanged(OmnibarMode? newValue) + { + CurrentSelectedModeName = newValue?.ModeName; + } + + partial void OnIsFocusedChanged(bool newValue) + { + //_textBox?.Focus(newValue ? FocusState.Programmatic : FocusState.Unfocused); + } } } diff --git a/src/Files.App.Controls/Omnibar/Omnibar.cs b/src/Files.App.Controls/Omnibar/Omnibar.cs index 9f3d5fae3710..fe8c2e20b2da 100644 --- a/src/Files.App.Controls/Omnibar/Omnibar.cs +++ b/src/Files.App.Controls/Omnibar/Omnibar.cs @@ -28,7 +28,6 @@ public partial class Omnibar : Control private Border _textBoxSuggestionsContainerBorder = null!; private ListView _textBoxSuggestionsListView = null!; - private bool _isFocused; private string _userInput = string.Empty; private OmnibarTextChangeReason _textChangeReason = OmnibarTextChangeReason.None; @@ -148,7 +147,7 @@ public void ChangeMode(OmnibarMode modeToExpand, bool shouldFocus = false, bool CurrentSelectedMode = modeToExpand; _textChangeReason = OmnibarTextChangeReason.ProgrammaticChange; - _textBox.Text = CurrentSelectedMode.Text ?? string.Empty; + ChangeTextBoxText(CurrentSelectedMode.Text ?? string.Empty); // Move cursor of the TextBox to the tail _textBox.Select(_textBox.Text.Length, 0); @@ -156,7 +155,7 @@ public void ChangeMode(OmnibarMode modeToExpand, bool shouldFocus = false, bool VisualStateManager.GoToState(CurrentSelectedMode, "Focused", true); CurrentSelectedMode.OnChangingCurrentMode(true); - if (_isFocused) + if (IsFocused) { VisualStateManager.GoToState(CurrentSelectedMode, "Focused", true); VisualStateManager.GoToState(_textBox, "InputAreaVisible", true); @@ -174,7 +173,7 @@ public void ChangeMode(OmnibarMode modeToExpand, bool shouldFocus = false, bool if (shouldFocus) _textBox.Focus(FocusState.Keyboard); - TryToggleIsSuggestionsPopupOpen(_isFocused && CurrentSelectedMode?.SuggestionItemsSource is not null); + TryToggleIsSuggestionsPopupOpen(IsFocused && CurrentSelectedMode?.SuggestionItemsSource is not null); // Remove the reposition transition from the all modes if (useTransition) @@ -189,7 +188,7 @@ public void ChangeMode(OmnibarMode modeToExpand, bool shouldFocus = false, bool public bool TryToggleIsSuggestionsPopupOpen(bool wantToOpen) { - if (wantToOpen && (!_isFocused || CurrentSelectedMode?.SuggestionItemsSource is null)) + if (wantToOpen && (!IsFocused || CurrentSelectedMode?.SuggestionItemsSource is null)) return false; _textBoxSuggestionsPopup.IsOpen = wantToOpen; @@ -205,10 +204,15 @@ public void ChooseSuggestionItem(object obj) if (CurrentSelectedMode.UpdateTextOnSelect) { _textChangeReason = OmnibarTextChangeReason.SuggestionChosen; - _textBox.Text = GetObjectText(obj); + ChangeTextBoxText(GetObjectText(obj)); } SuggestionChosen?.Invoke(this, new(CurrentSelectedMode, obj)); + } + + internal protected void ChangeTextBoxText(string text) + { + _textBox.Text = text; // Move the cursor to the end of the TextBox _textBox?.Select(_textBox.Text.Length, 0); @@ -233,7 +237,7 @@ private string GetObjectText(object obj) return obj is string text ? text : obj is IOmnibarTextMemberPathProvider textMemberPathProvider - ? textMemberPathProvider.GetTextMemberPath(CurrentSelectedMode.DisplayMemberPath ?? string.Empty) + ? textMemberPathProvider.GetTextMemberPath(CurrentSelectedMode.TextMemberPath ?? string.Empty) : obj.ToString() ?? string.Empty; } @@ -245,10 +249,7 @@ private void RevertTextToUserInput() _textBoxSuggestionsListView.SelectedIndex = -1; _textChangeReason = OmnibarTextChangeReason.ProgrammaticChange; - _textBox.Text = _userInput ?? ""; - - // Move the cursor to the end of the TextBox - _textBox?.Select(_textBox.Text.Length, 0); + ChangeTextBoxText(_userInput ?? ""); } } } diff --git a/src/Files.App.Controls/Omnibar/Omnibar.xaml b/src/Files.App.Controls/Omnibar/Omnibar.xaml index 439ec2674897..64eabed99947 100644 --- a/src/Files.App.Controls/Omnibar/Omnibar.xaml +++ b/src/Files.App.Controls/Omnibar/Omnibar.xaml @@ -133,7 +133,6 @@ Height="{TemplateBinding Height}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Background="{TemplateBinding Background}" - CornerRadius="{TemplateBinding CornerRadius}" TabFocusNavigation="Local"> + /// Implement in to get the text member path from the suggestion item correctly. + /// + public partial string? TextMemberPath { get; set; } [GeneratedDependencyProperty(DefaultValue = true)] public partial bool UpdateTextOnSelect { get; set; } + + partial void OnTextChanged(string? newValue) + { + if (_ownerRef is null || _ownerRef.TryGetTarget(out var owner) is false) + return; + + owner.ChangeTextBoxText(newValue ?? string.Empty); + } } } diff --git a/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs index 46912d9ed444..35970397d594 100644 --- a/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs +++ b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs @@ -3,6 +3,7 @@ namespace Files.App.Data.Items { + [Obsolete("Remove once Omnibar goes out of experimental.")] public sealed partial class NavigationBarSuggestionItem : ObservableObject { private string? _Text; diff --git a/src/Files.App/Data/Models/BreadcrumbBarItemModel.cs b/src/Files.App/Data/Models/BreadcrumbBarItemModel.cs new file mode 100644 index 000000000000..d8620ca2ba79 --- /dev/null +++ b/src/Files.App/Data/Models/BreadcrumbBarItemModel.cs @@ -0,0 +1,7 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Data.Models +{ + internal record BreadcrumbBarItemModel(string Text); +} diff --git a/src/Files.App/Data/Models/OmnibarPathModeSuggestionModel.cs b/src/Files.App/Data/Models/OmnibarPathModeSuggestionModel.cs new file mode 100644 index 000000000000..4142d00f0941 --- /dev/null +++ b/src/Files.App/Data/Models/OmnibarPathModeSuggestionModel.cs @@ -0,0 +1,20 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Controls; + +namespace Files.App.Data.Models +{ + internal record OmnibarPathModeSuggestionModel(string Path, string DisplayName) : IOmnibarTextMemberPathProvider + { + public string GetTextMemberPath(string textMemberPath) + { + return textMemberPath switch + { + nameof(Path) => Path, + nameof(DisplayName) => DisplayName, + _ => string.Empty + }; + } + } +} diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index a9485ecbc3dd..e91e543ef9ea 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -4199,4 +4199,7 @@ You can add sections to the sidebar by right-clicking and selecting the sections you want to add. + + Enter a path to navigate to... + \ No newline at end of file diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml b/src/Files.App/UserControls/NavigationToolbar.xaml index 1473c10abaa2..7fe8371ec8e5 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml +++ b/src/Files.App/UserControls/NavigationToolbar.xaml @@ -9,6 +9,8 @@ xmlns:converters="using:Files.App.Converters" xmlns:converters1="using:CommunityToolkit.WinUI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dataitems="using:Files.App.Data.Items" + xmlns:datamodels="using:Files.App.Data.Models" xmlns:helpers="using:Files.App.Helpers" xmlns:items="using:Files.App.Data.Items" xmlns:keyboard="using:Files.App.UserControls.KeyboardShortcut" @@ -211,6 +213,7 @@ + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (HistoryItemClicked); } + // Methods + private void NavToolbar_Loading(FrameworkElement _, object e) { Loading -= NavToolbar_Loading; @@ -104,7 +99,7 @@ private void VisiblePath_LostFocus(object _, RoutedEventArgs e) if (App.AppModel.IsMainWindowClosed) return; - var element = FocusManager.GetFocusedElement(MainWindow.Instance.Content.XamlRoot); + var element = Microsoft.UI.Xaml.Input.FocusManager.GetFocusedElement(MainWindow.Instance.Content.XamlRoot); if (element is FlyoutBase or AppBarButton or Popup) return; @@ -195,9 +190,9 @@ private async Task AddHistoryItemsAsync(IEnumerable items, IList var flyoutItem = new MenuFlyoutItem { - Icon = new FontIcon { Glyph = "\uE8B7" }, // Use font icon as placeholder + Icon = new FontIcon() { Glyph = "\uE8B7" }, // Placeholder icon Text = fileName, - Command = historyItemClickedCommand, + Command = new RelayCommand(HistoryItemClicked), CommandParameter = new ToolbarHistoryItemModel(item, isBackMode) }; @@ -205,58 +200,115 @@ private async Task AddHistoryItemsAsync(IEnumerable items, IList // Start loading the thumbnail in the background _ = LoadFlyoutItemIconAsync(flyoutItem, args.NavPathParam); + + async Task LoadFlyoutItemIconAsync(MenuFlyoutItem flyoutItem, string path) + { + var imageSource = await NavigationHelpers.GetIconForPathAsync(path); + + if (imageSource is not null) + flyoutItem.Icon = new ImageIcon() { Source = imageSource }; + } + + void HistoryItemClicked(ToolbarHistoryItemModel? itemModel) + { + if (itemModel is null) + return; + + var shellPage = Ioc.Default.GetRequiredService().ShellPage; + if (shellPage is null) + return; + + if (itemModel.IsBackMode) + { + // Remove all entries after the target entry in the BackwardStack + while (shellPage.BackwardStack.Last() != itemModel.PageStackEntry) + { + shellPage.BackwardStack.RemoveAt(shellPage.BackwardStack.Count - 1); + } + + // Navigate back + shellPage.Back_Click(); + } + else + { + // Remove all entries before the target entry in the ForwardStack + while (shellPage.ForwardStack.First() != itemModel.PageStackEntry) + { + shellPage.ForwardStack.RemoveAt(0); + } + + // Navigate forward + shellPage.Forward_Click(); + } + } } } - private async Task LoadFlyoutItemIconAsync(MenuFlyoutItem flyoutItem, string path) + private void ClickablePath_GettingFocus(UIElement sender, GettingFocusEventArgs args) { - var imageSource = await NavigationHelpers.GetIconForPathAsync(path); + if (args.InputDevice != FocusInputDeviceKind.Keyboard) + return; - if (imageSource is not null) - flyoutItem.Icon = new ImageIcon { Source = imageSource }; + var previousControl = args.OldFocusedElement as FrameworkElement; + if (previousControl?.Name == nameof(HomeButton) || previousControl?.Name == nameof(Refresh)) + ViewModel.IsEditModeEnabled = true; } - private void HistoryItemClicked(ToolbarHistoryItemModel? itemModel) + private async void Omnibar_QuerySubmitted(Omnibar sender, OmnibarQuerySubmittedEventArgs args) { - if (itemModel is null) - return; + await ViewModel.HandleItemNavigationAsync(args.Text); + } - var shellPage = Ioc.Default.GetRequiredService().ShellPage; - if (shellPage is null) - return; + private async void Omnibar_SuggestionChosen(Omnibar sender, OmnibarSuggestionChosenEventArgs args) + { + if (args.SelectedItem is OmnibarPathModeSuggestionModel item && + !string.IsNullOrEmpty(item.Path)) + await ViewModel.HandleItemNavigationAsync(item.Path); + } - if (itemModel.IsBackMode) - { - // Remove all entries after the target entry in the BackwardStack - while (shellPage.BackwardStack.Last() != itemModel.PageStackEntry) - { - shellPage.BackwardStack.RemoveAt(shellPage.BackwardStack.Count - 1); - } + private async void Omnibar_TextChanged(Omnibar sender, OmnibarTextChangedEventArgs args) + { + await ViewModel.PopulateOmnibarSuggestionsForPathMode(); + } - // Navigate back - shellPage.Back_Click(); - } - else + private async void BreadcrumbBar_ItemClicked(Controls.BreadcrumbBar sender, Controls.BreadcrumbBarItemClickedEventArgs args) + { + if (args.IsRootItem) { - // Remove all entries before the target entry in the ForwardStack - while (shellPage.ForwardStack.First() != itemModel.PageStackEntry) - { - shellPage.ForwardStack.RemoveAt(0); - } - - // Navigate forward - shellPage.Forward_Click(); + await ViewModel.HandleItemNavigationAsync("Home"); + return; } + + await ViewModel.HandleFolderNavigationAsync(ViewModel.PathComponents[args.Index].Path); } - private void ClickablePath_GettingFocus(UIElement sender, GettingFocusEventArgs args) + private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, BreadcrumbBarItemDropDownFlyoutEventArgs e) { - if (args.InputDevice != FocusInputDeviceKind.Keyboard) + if (e.IsRootItem) + { + // TODO: Populate a different flyout for the root item + e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = "Quick access" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Desktop" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Download" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Documents" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Pictures" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Music" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Videos" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Recycle bin" }); + e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = "Drives" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Local Disk (C:)" }); + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Local Disk (D:)" }); + return; + } - var previousControl = args.OldFocusedElement as FrameworkElement; - if (previousControl?.Name == nameof(HomeButton) || previousControl?.Name == nameof(Refresh)) - ViewModel.IsEditModeEnabled = true; + await ViewModel.SetPathBoxDropDownFlyoutAsync(e.Flyout, ViewModel.PathComponents[e.Index]); + } + + private void BreadcrumbBar_ItemDropDownFlyoutClosed(object sender, BreadcrumbBarItemDropDownFlyoutEventArgs e) + { + // Clear the flyout items to save memory + e.Flyout.Items.Clear(); } } } diff --git a/src/Files.App/UserControls/PathBreadcrumb.xaml.cs b/src/Files.App/UserControls/PathBreadcrumb.xaml.cs index bb08408b5eaa..66726ee9e82b 100644 --- a/src/Files.App/UserControls/PathBreadcrumb.xaml.cs +++ b/src/Files.App/UserControls/PathBreadcrumb.xaml.cs @@ -50,7 +50,15 @@ private async void PathBoxItem_Drop(object sender, DragEventArgs e) private async void PathBoxItem_Tapped(object sender, TappedRoutedEventArgs e) { - await ViewModel.PathBoxItem_Tapped(sender, e); + if (sender is not TextBlock textBlock || + textBlock.DataContext is not PathBoxItem item || + item.Path is not { } path) + return; + + // TODO: Implement middle click retrieving. + await ViewModel.HandleFolderNavigationAsync(path); + + e.Handled = true; } private void PathBoxItem_PointerPressed(object sender, PointerRoutedEventArgs e) diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs index 05e632463d26..4b588a54167a 100644 --- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.WinUI; using Files.App.Actions; +using Files.App.Controls; using Files.Shared.Helpers; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -22,6 +23,10 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr private const int MaxSuggestionsCount = 10; + public const string OmnibarPathModeName = "Path"; + public const string OmnibarPaletteModeName = "Palette"; + public const string OmnibarSearchModeName = "Search"; + // Dependency injections private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService(); @@ -30,6 +35,7 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr private readonly DrivesViewModel drivesViewModel = Ioc.Default.GetRequiredService(); private readonly IUpdateService UpdateService = Ioc.Default.GetRequiredService(); private readonly ICommandManager Commands = Ioc.Default.GetRequiredService(); + private readonly IContentPageContext ContentPageContext = Ioc.Default.GetRequiredService(); // Fields @@ -63,6 +69,8 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr public ObservableCollection NavigationBarSuggestions { get; } = []; + internal ObservableCollection PathModeSuggestionItems { get; } = []; + public bool IsSingleItemOverride { get; set; } public bool SearchHasFocus { get; private set; } @@ -199,16 +207,55 @@ public bool IsSearchBoxVisible } private string? _PathText; + [Obsolete("Remove once Omnibar goes out of experimental.")] public string? PathText { get => _PathText; set { - _PathText = value; - OnPropertyChanged(nameof(PathText)); + if (SetProperty(ref _PathText, value)) + OnPropertyChanged(nameof(OmnibarPathModeText)); + } + } + + private bool _IsOmnibarFocused; + public bool IsOmnibarFocused + { + get => _IsOmnibarFocused; + set + { + if (SetProperty(ref _IsOmnibarFocused, value)) + { + if (value) + { + switch(OmnibarCurrentSelectedModeName) + { + case OmnibarPathModeName: + OmnibarPathModeText = + string.IsNullOrEmpty(ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory) + ? Constants.UserEnvironmentPaths.HomePath + : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + _ = PopulateOmnibarSuggestionsForPathMode(); + break; + case OmnibarPaletteModeName: + break; + case OmnibarSearchModeName: + break; + default: + throw new ArgumentOutOfRangeException(""); + } + + } + } } } + private string _OmnibarCurrentSelectedModeName; + public string OmnibarCurrentSelectedModeName { get => _OmnibarCurrentSelectedModeName; set => SetProperty(ref _OmnibarCurrentSelectedModeName, value); } + + private string _OmnibarPathModeText; + public string OmnibarPathModeText { get => _OmnibarPathModeText; set => SetProperty(ref _OmnibarPathModeText, value); } + private CurrentInstanceViewModel _InstanceViewModel; public CurrentInstanceViewModel InstanceViewModel { @@ -226,6 +273,7 @@ public CurrentInstanceViewModel InstanceViewModel } } + [Obsolete("Remove once Omnibar goes out of experimental.")] public bool IsEditModeEnabled { get => ManualEntryBoxLoaded; @@ -548,28 +596,124 @@ public void PathBoxItem_PointerPressed(object sender, PointerRoutedEventArgs e) _pointerRoutedEventArgs = ptrPt.Properties.IsMiddleButtonPressed ? e : null; } - public async Task PathBoxItem_Tapped(object sender, TappedRoutedEventArgs e) + public async Task HandleFolderNavigationAsync(string path, bool openNewTab = false) { - var itemTappedPath = ((sender as TextBlock)?.DataContext as PathBoxItem)?.Path; - if (itemTappedPath is null) - return; - - if (_pointerRoutedEventArgs is not null) + openNewTab |= _pointerRoutedEventArgs is not null; + if (openNewTab) { - await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => - { - await NavigationHelpers.AddNewTabByPathAsync(typeof(ShellPanesPage), itemTappedPath, true); - }, DispatcherQueuePriority.Low); - e.Handled = true; + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync( + async () => + { + await NavigationHelpers.AddNewTabByPathAsync(typeof(ShellPanesPage), path, true); + }, + DispatcherQueuePriority.Low); + _pointerRoutedEventArgs = null; return; } - ToolbarPathItemInvoked?.Invoke(this, new PathNavigationEventArgs() + ToolbarPathItemInvoked?.Invoke(this, new() { ItemPath = path }); + } + + public async Task HandleItemNavigationAsync(string path) + { + if (ContentPageContext.ShellPage is null || PathComponents.LastOrDefault()?.Path is not { } currentPath) + return; + + var isFtp = FtpHelpers.IsFtpPath(path); + var normalizedInput = NormalizePathInput(path, isFtp); + if (currentPath.Equals(normalizedInput, StringComparison.OrdinalIgnoreCase) || + string.IsNullOrWhiteSpace(normalizedInput)) + return; + + if (normalizedInput.Equals(ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory) && + ContentPageContext.ShellPage.CurrentPageType != typeof(HomePage)) + return; + + if (normalizedInput.Equals("Home", StringComparison.OrdinalIgnoreCase) || + normalizedInput.Equals(Strings.Home.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) { - ItemPath = itemTappedPath - }); + SavePathToHistory("Home"); + ContentPageContext.ShellPage.NavigateHome(); + } + else if (normalizedInput.Equals("ReleaseNotes", StringComparison.OrdinalIgnoreCase) || + normalizedInput.Equals(Strings.ReleaseNotes.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) + { + SavePathToHistory("ReleaseNotes"); + ContentPageContext.ShellPage.NavigateToReleaseNotes(); + } + else if (normalizedInput.Equals("Settings", StringComparison.OrdinalIgnoreCase) || + normalizedInput.Equals(Strings.Settings.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) + { + //SavePathToHistory("Settings"); + //ContentPageContext.ShellPage.NavigateToSettings(); + } + else + { + normalizedInput = StorageFileExtensions.GetResolvedPath(normalizedInput, isFtp); + if (currentPath.Equals(normalizedInput, StringComparison.OrdinalIgnoreCase)) + return; + + var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(normalizedInput)); + + var resFolder = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(normalizedInput, item)); + if (resFolder || FolderHelpers.CheckFolderAccessWithWin32(normalizedInput)) + { + var matchingDrive = drivesViewModel.Drives.Cast().FirstOrDefault(x => PathNormalization.NormalizePath(normalizedInput).StartsWith(PathNormalization.NormalizePath(x.Path), StringComparison.Ordinal)); + if (matchingDrive is not null && matchingDrive.Type == Data.Items.DriveType.CDRom && matchingDrive.MaxSpace == ByteSizeLib.ByteSize.FromBytes(0)) + { + bool ejectButton = await DialogDisplayHelper.ShowDialogAsync(Strings.InsertDiscDialog_Title.GetLocalizedResource(), string.Format(Strings.InsertDiscDialog_Text.GetLocalizedResource(), matchingDrive.Path), Strings.InsertDiscDialog_OpenDriveButton.GetLocalizedResource(), Strings.Close.GetLocalizedResource()); + if (ejectButton) + DriveHelpers.EjectDeviceAsync(matchingDrive.Path); + return; + } + + var pathToNavigate = resFolder.Result?.Path ?? normalizedInput; + SavePathToHistory(pathToNavigate); + ContentPageContext.ShellPage.NavigateToPath(pathToNavigate); + } + else if (isFtp) + { + SavePathToHistory(normalizedInput); + ContentPageContext.ShellPage.NavigateToPath(normalizedInput); + } + else // Not a folder or inaccessible + { + var resFile = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileWithPathFromPathAsync(normalizedInput, item)); + if (resFile) + { + var pathToInvoke = resFile.Result.Path; + await Win32Helper.InvokeWin32ComponentAsync(pathToInvoke, ContentPageContext.ShellPage); + } + else // Not a file or not accessible + { + var workingDir = + string.IsNullOrEmpty(ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory) || + ContentPageContext.ShellPage.CurrentPageType == typeof(HomePage) + ? Constants.UserEnvironmentPaths.HomePath + : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + + if (await LaunchApplicationFromPath(OmnibarPathModeText, workingDir)) + return; + + try + { + if (!await Windows.System.Launcher.LaunchUriAsync(new Uri(OmnibarPathModeText))) + await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidItemDialogTitle.GetLocalizedResource(), + string.Format(Strings.InvalidItemDialogContent.GetLocalizedResource(), Environment.NewLine, resFolder.ErrorCode.ToString())); + } + catch (Exception ex) when (ex is UriFormatException || ex is ArgumentException) + { + await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidItemDialogTitle.GetLocalizedResource(), + string.Format(Strings.InvalidItemDialogContent.GetLocalizedResource(), Environment.NewLine, resFolder.ErrorCode.ToString())); + } + } + } + } + + PathControlDisplayText = ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + IsOmnibarFocused = false; } public void PathBoxItem_PreviewKeyDown(object sender, KeyRoutedEventArgs e) @@ -677,12 +821,12 @@ public void SearchRegion_LostFocus(object sender, RoutedEventArgs e) private void SearchRegion_Escaped(object? sender, ISearchBoxViewModel _SearchBox) => CloseSearchBox(true); - public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem pathItem, IShellPage shellPage) + public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem pathItem) { var nextPathItemTitle = PathComponents[PathComponents.IndexOf(pathItem) + 1].Title; IList? childFolders = null; - StorageFolderWithPath folder = await shellPage.ShellViewModel.GetFolderWithPathFromPathAsync(pathItem.Path); + StorageFolderWithPath folder = await ContentPageContext.ShellPage.ShellViewModel.GetFolderWithPathFromPathAsync(pathItem.Path); if (folder is not null) childFolders = (await FilesystemTasks.Wrap(() => folder.GetFoldersWithPathAsync(string.Empty))).Result; @@ -723,7 +867,7 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p flyoutItem.Click += (sender, args) => { // Navigate to the directory - shellPage.NavigateToPath(childFolder.Path); + ContentPageContext.ShellPage.NavigateToPath(childFolder.Path); }; } @@ -755,6 +899,7 @@ private static string NormalizePathInput(string currentInput, bool isFtp) return currentInput; } + [Obsolete("Remove once Omnibar goes out of experimental.")] public async Task CheckPathInputAsync(string currentInput, string currentSelectedPath, IShellPage shellPage) { if (currentInput.StartsWith('>')) @@ -885,6 +1030,107 @@ private static async Task LaunchApplicationFromPath(string currentInput, s ); } + public async Task PopulateOmnibarSuggestionsForPathMode() + { + var result = await SafetyExtensions.IgnoreExceptions(async () => + { + List? newSuggestions = []; + var pathText = OmnibarPathModeText; + + // If the current input is special, populate navigation history instead. + if (string.IsNullOrWhiteSpace(pathText) || + pathText is "Home" or "ReleaseNotes" or "Settings") + { + // Load previously entered path + if (UserSettingsService.GeneralSettingsService.PathHistoryList is { } pathHistoryList) + { + newSuggestions.AddRange(pathHistoryList.Select(x => new OmnibarPathModeSuggestionModel(x, x))); + } + } + else + { + var isFtp = FtpHelpers.IsFtpPath(pathText); + pathText = NormalizePathInput(pathText, isFtp); + var expandedPath = StorageFileExtensions.GetResolvedPath(pathText, isFtp); + var folderPath = PathNormalization.GetParentDir(expandedPath) ?? expandedPath; + StorageFolderWithPath folder = await ContentPageContext.ShellPage.ShellViewModel.GetFolderWithPathFromPathAsync(folderPath); + if (folder is null) + return false; + + var currPath = await folder.GetFoldersWithPathAsync(Path.GetFileName(expandedPath), MaxSuggestionsCount); + if (currPath.Count >= MaxSuggestionsCount) + { + newSuggestions.AddRange(currPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, x.Item.DisplayName))); + } + else if (currPath.Any()) + { + var subPath = await currPath.First().GetFoldersWithPathAsync((uint)(MaxSuggestionsCount - currPath.Count)); + newSuggestions.AddRange(currPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, x.Item.DisplayName))); + newSuggestions.AddRange(subPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, PathNormalization.Combine(currPath.First().Item.DisplayName, x.Item.DisplayName)))); + } + } + + // If there are no suggestions, show "No suggestions" + if (newSuggestions.Count is 0) + { + AddNoResultsItem(); + } + + // Check whether at least one item is in common between the old and the new suggestions + // since the suggestions popup becoming empty causes flickering + if (!PathModeSuggestionItems.IntersectBy(newSuggestions, x => x.DisplayName).Any()) + { + // No items in common, update the list in-place + for (int index = 0; index < newSuggestions.Count; index++) + { + if (index < PathModeSuggestionItems.Count) + { + PathModeSuggestionItems[index] = newSuggestions[index]; + } + else + { + PathModeSuggestionItems.Add(newSuggestions[index]); + } + } + + while (PathModeSuggestionItems.Count > newSuggestions.Count) + PathModeSuggestionItems.RemoveAt(PathModeSuggestionItems.Count - 1); + } + else + { + // At least an element in common, show animation + foreach (var s in PathModeSuggestionItems.ExceptBy(newSuggestions, x => x.DisplayName).ToList()) + PathModeSuggestionItems.Remove(s); + + for (int index = 0; index < newSuggestions.Count; index++) + { + if (PathModeSuggestionItems.Count > index && PathModeSuggestionItems[index].DisplayName == newSuggestions[index].DisplayName) + { + PathModeSuggestionItems[index] = newSuggestions[index]; + } + else + PathModeSuggestionItems.Insert(index, newSuggestions[index]); + } + } + + return true; + }); + + if (!result) + { + AddNoResultsItem(); + } + + void AddNoResultsItem() + { + PathModeSuggestionItems.Clear(); + PathModeSuggestionItems.Add(new( + ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory, + Strings.NavigationToolbarVisiblePathNoResults.GetLocalizedResource())); + } + } + + [Obsolete("Remove once Omnibar goes out of experimental.")] public async Task SetAddressBarSuggestionsAsync(AutoSuggestBox sender, IShellPage shellpage) { if (sender.Text is not null && shellpage.ShellViewModel is not null) diff --git a/src/Files.App/Views/Shells/BaseShellPage.cs b/src/Files.App/Views/Shells/BaseShellPage.cs index 76000bad73c3..07d4fec378eb 100644 --- a/src/Files.App/Views/Shells/BaseShellPage.cs +++ b/src/Files.App/Views/Shells/BaseShellPage.cs @@ -422,7 +422,7 @@ protected async void ShellPage_AddressBarTextEntered(object sender, AddressBarTe protected async void ShellPage_ToolbarPathItemLoaded(object sender, ToolbarPathItemLoadedEventArgs e) { - await ToolbarViewModel.SetPathBoxDropDownFlyoutAsync(e.OpenedFlyout, e.Item, this); + await ToolbarViewModel.SetPathBoxDropDownFlyoutAsync(e.OpenedFlyout, e.Item); } protected async void ShellPage_ToolbarFlyoutOpening(object sender, ToolbarFlyoutOpeningEventArgs e) @@ -430,7 +430,7 @@ protected async void ShellPage_ToolbarFlyoutOpening(object sender, ToolbarFlyout var pathBoxItem = ((Button)e.OpeningFlyout.Target).DataContext as PathBoxItem; if (pathBoxItem is not null) - await ToolbarViewModel.SetPathBoxDropDownFlyoutAsync(e.OpeningFlyout, pathBoxItem, this); + await ToolbarViewModel.SetPathBoxDropDownFlyoutAsync(e.OpeningFlyout, pathBoxItem); } protected async void NavigationToolbar_QuerySubmitted(object sender, ToolbarQuerySubmittedEventArgs e) @@ -442,9 +442,10 @@ protected void NavigationToolbar_EditModeEnabled(object sender, EventArgs e) { ToolbarViewModel.ManualEntryBoxLoaded = true; ToolbarViewModel.ClickablePathLoaded = false; - ToolbarViewModel.PathText = string.IsNullOrEmpty(ShellViewModel?.WorkingDirectory) + ToolbarViewModel.OmnibarPathModeText = string.IsNullOrEmpty(ShellViewModel?.WorkingDirectory) ? Constants.UserEnvironmentPaths.HomePath : ShellViewModel.WorkingDirectory; + ToolbarViewModel.PathText = ToolbarViewModel.OmnibarPathModeText; } protected async void DrivesManager_PropertyChanged(object sender, PropertyChangedEventArgs e) From 686264e53626c64ec30bdfe607fb80f936ca8d88 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:37:53 +0900 Subject: [PATCH 2/3] Replaced the placeholder with the actual data sources --- .../Storables/HomeFolder/HomeFolder.cs | 49 +++++---- .../WindowsStorage/IWindowsStorable.cs | 2 +- .../Storables/WindowsStorage/WindowsFile.cs | 10 +- .../Storables/WindowsStorage/WindowsFolder.cs | 103 ++++++++---------- .../WindowsStorage/WindowsStorable.cs | 8 +- .../WindowsStorableHelpers.Shell.cs | 2 +- .../UserControls/NavigationToolbar.xaml.cs | 28 +++-- 7 files changed, 97 insertions(+), 105 deletions(-) diff --git a/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs b/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs index 879cdbf9f2bd..790cbc17c13e 100644 --- a/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs +++ b/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs @@ -46,37 +46,40 @@ public IAsyncEnumerable GetQuickAccessFolderAsync(CancellationTo } /// - public async IAsyncEnumerable GetLogicalDrivesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + public IAsyncEnumerable GetLogicalDrivesAsync(CancellationToken cancellationToken = default) { - var availableDrives = PInvoke.GetLogicalDrives(); - if (availableDrives is 0) - yield break; + return GetLogicalDrives().ToAsyncEnumerable(); - int count = BitOperations.PopCount(availableDrives); - var driveLetters = new char[count]; - - count = 0; - char driveLetter = 'A'; - while (availableDrives is not 0) + IEnumerable GetLogicalDrives() { - if ((availableDrives & 1) is not 0) - driveLetters[count++] = driveLetter; + var availableDrives = PInvoke.GetLogicalDrives(); + if (availableDrives is 0) + yield break; - availableDrives >>= 1; - driveLetter++; - } + int count = BitOperations.PopCount(availableDrives); + var driveLetters = new char[count]; - foreach (int letter in driveLetters) - { - cancellationToken.ThrowIfCancellationRequested(); + count = 0; + char driveLetter = 'A'; + while (availableDrives is not 0) + { + if ((availableDrives & 1) is not 0) + driveLetters[count++] = driveLetter; - if (WindowsStorable.TryParse($"{letter}:\\") is not IWindowsStorable driveRoot) - throw new InvalidOperationException(); + availableDrives >>= 1; + driveLetter++; + } - yield return new WindowsFolder(driveRoot.ThisPtr); - await Task.Yield(); - } + foreach (char letter in driveLetters) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (WindowsStorable.TryParse($"{letter}:\\") is not IWindowsStorable driveRoot) + throw new InvalidOperationException(); + yield return new WindowsFolder(driveRoot.ThisPtr); + } + } } /// diff --git a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs index 788989ca8658..ed3d49815e94 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs @@ -6,7 +6,7 @@ namespace Files.App.Storage { - public interface IWindowsStorable + public interface IWindowsStorable: IDisposable { ComPtr ThisPtr { get; } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs index ee89b06651d3..3ce56f786c2f 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs @@ -8,7 +8,7 @@ namespace Files.App.Storage { [DebuggerDisplay("{" + nameof(ToString) + "()}")] - public sealed class WindowsFile : WindowsStorable, IChildFile, IDisposable + public sealed class WindowsFile : WindowsStorable, IChildFile { public WindowsFile(ComPtr nativeObject) { @@ -19,13 +19,5 @@ public Task OpenStreamAsync(FileAccess accessMode, CancellationToken can { throw new NotImplementedException(); } - - // Disposer - - /// - public void Dispose() - { - ThisPtr.Dispose(); - } } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs index ff466953daa7..f4105687184f 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs @@ -9,8 +9,8 @@ namespace Files.App.Storage { - [DebuggerDisplay("{" + nameof(ToString) + "()")] - public sealed class WindowsFolder : WindowsStorable, IChildFolder, IDisposable + [DebuggerDisplay("{" + nameof(ToString) + "()}")] + public sealed class WindowsFolder : WindowsStorable, IChildFolder { public WindowsFolder(ComPtr nativeObject) { @@ -26,81 +26,64 @@ public unsafe WindowsFolder(IShellItem* nativeObject) public unsafe WindowsFolder(Guid folderId) { - Guid folderIdLocal = folderId; - Guid IID_IShellItem = IShellItem.IID_Guid; ComPtr pItem = default; - HRESULT hr = PInvoke.SHGetKnownFolderItem(&folderIdLocal, KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HANDLE.Null, &IID_IShellItem, (void**)pItem.GetAddressOf()); - if (hr.Succeeded) + HRESULT hr = PInvoke.SHGetKnownFolderItem(&folderId, KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HANDLE.Null, IID.IID_IShellItem, (void**)pItem.GetAddressOf()); + if (hr.Failed) { - ThisPtr = pItem; - return; - } + fixed (char* pszShellPath = $"Shell:::{folderId:B}") + hr = PInvoke.SHCreateItemFromParsingName(pszShellPath, null, IID.IID_IShellItem, (void**)pItem.GetAddressOf()); - fixed (char* pszShellPath = $"Shell:::{folderId:B}") - hr = PInvoke.SHCreateItemFromParsingName(pszShellPath, null, &IID_IShellItem, (void**)pItem.GetAddressOf()); - if (hr.Succeeded) - { - ThisPtr = pItem; - return; + // Invalid FOLDERID; this should never happen. + hr.ThrowOnFailure(); } + + ThisPtr = pItem; } - public async IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.All, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.All, CancellationToken cancellationToken = default) { - using ComPtr pEnumShellItems = GetEnumShellItems(); - while (GetNext(pEnumShellItems) is { } pShellItem && !pShellItem.IsNull) - { - cancellationToken.ThrowIfCancellationRequested(); + return GetItems().ToAsyncEnumerable(); - var pShellFolder = pShellItem.As(); - var isFolder = IsFolder(pShellItem); + unsafe IEnumerable GetItems() + { + ComPtr pEnumShellItems = default; + GetEnumerator(); - if (type is StorableType.File && !isFolder) + ComPtr pShellItem = default; + while (GetNext() && !pShellItem.IsNull) { - yield return new WindowsFile(pShellItem); + cancellationToken.ThrowIfCancellationRequested(); + var isFolder = pShellItem.HasShellAttributes(SFGAO_FLAGS.SFGAO_FOLDER); + + if (type is StorableType.File && !isFolder) + { + yield return new WindowsFile(pShellItem); + } + else if (type is StorableType.Folder && isFolder) + { + yield return new WindowsFolder(pShellItem); + } + else + { + continue; + } } - else if (type is StorableType.Folder && isFolder) + + yield break; + + unsafe void GetEnumerator() { - yield return new WindowsFile(pShellItem); + HRESULT hr = ThisPtr.Get()->BindToHandler(null, BHID.BHID_EnumItems, IID.IID_IEnumShellItems, (void**)pEnumShellItems.GetAddressOf()); + hr.ThrowIfFailedOnDebug(); } - else + + unsafe bool GetNext() { - continue; + HRESULT hr = pEnumShellItems.Get()->Next(1, pShellItem.GetAddressOf()); + return hr.ThrowIfFailedOnDebug() == HRESULT.S_OK; } - - await Task.Yield(); } - - unsafe ComPtr GetEnumShellItems() - { - ComPtr pEnumShellItems = default; - Guid IID_IEnumShellItems = typeof(IEnumShellItems).GUID; - Guid BHID_EnumItems = PInvoke.BHID_EnumItems; - HRESULT hr = ThisPtr.Get()->BindToHandler(null, &BHID_EnumItems, &IID_IEnumShellItems, (void**)pEnumShellItems.GetAddressOf()); - return pEnumShellItems; - } - - unsafe ComPtr GetNext(ComPtr pEnumShellItems) - { - ComPtr pShellItem = default; - HRESULT hr = pEnumShellItems.Get()->Next(1, pShellItem.GetAddressOf()); - return pShellItem; - } - - unsafe bool IsFolder(ComPtr pShellItem) - { - return pShellItem.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var specifiedAttribute).Succeeded && - specifiedAttribute is SFGAO_FLAGS.SFGAO_FOLDER; - } - } - - // Disposer - - /// - public void Dispose() - { - ThisPtr.Dispose(); } } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs index 92c45b64f90c..c203e517cb27 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs @@ -10,7 +10,7 @@ namespace Files.App.Storage { public abstract class WindowsStorable : IWindowsStorable, IStorableChild { - public ComPtr ThisPtr { get; protected set; } = default; + public ComPtr ThisPtr { get; protected set; } public string Id => this.GetDisplayName(SIGDN.SIGDN_FILESYSPATH); @@ -65,6 +65,12 @@ public abstract class WindowsStorable : IWindowsStorable, IStorableChild return Task.FromResult(new WindowsFolder(pParentFolder)); } + /// + public void Dispose() + { + ThisPtr.Dispose(); + } + /// public override string ToString() { diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs index 375c9deb3a07..6bfdad26f7ed 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs @@ -48,7 +48,7 @@ public unsafe static string GetDisplayName(this IWindowsStorable storable, SIGDN HRESULT hr = storable.ThisPtr.Get()->GetDisplayName(options, (PWSTR*)pszName.GetAddressOf()); return hr.ThrowIfFailedOnDebug().Succeeded - ? (*pszName.Get()).ToString() + ? new string((char*)pszName.Get()) : string.Empty; } } diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs index 0d6d4cb0a295..2f8a8a46b409 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs +++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs @@ -286,18 +286,26 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc { if (e.IsRootItem) { - // TODO: Populate a different flyout for the root item + IHomeFolder homeFolder = new HomeFolder(); + e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = "Quick access" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Desktop" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Download" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Documents" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Pictures" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Music" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Videos" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Recycle bin" }); + + await foreach (var storable in homeFolder.GetQuickAccessFolderAsync()) + { + if (storable is not IWindowsStorable windowsStorable) + continue; + + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = windowsStorable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI) }); + windowsStorable.Dispose(); + } + e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = "Drives" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Local Disk (C:)" }); - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = "Local Disk (D:)" }); + + await foreach (IWindowsStorable storable in homeFolder.GetLogicalDrivesAsync()) + { + e.Flyout.Items.Add(new MenuFlyoutItem() { Text = storable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI) }); + storable.Dispose(); + } return; } From 6465306d107e49ebfff133f244e09ccb88da14b8 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 24 Apr 2025 02:03:54 +0900 Subject: [PATCH 3/3] A rough impl to get thumbnail of the items for the root item chevron flyout --- .../UserControls/NavigationToolbar.xaml.cs | 31 ++++++++++++++++--- .../NavigationToolbarViewModel.cs | 2 -- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs index 2f8a8a46b409..3261f59c15d4 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs +++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs @@ -295,16 +295,39 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc if (storable is not IWindowsStorable windowsStorable) continue; - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = windowsStorable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI) }); + var flyoutItem = new MenuFlyoutItem() + { + Text = windowsStorable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI), + Icon = new FontIcon { Glyph = "\uE8B7" }, // Use font icon as placeholder + }; + + e.Flyout.Items.Add(flyoutItem); + + windowsStorable.TryGetThumbnail((int)(16f * App.AppModel.AppWindowDPI), Windows.Win32.UI.Shell.SIIGBF.SIIGBF_ICONONLY, out var thumbnailData); + flyoutItem.Icon = new ImageIcon() { Source = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => thumbnailData.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal) }; + windowsStorable.Dispose(); } e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = "Drives" }); - await foreach (IWindowsStorable storable in homeFolder.GetLogicalDrivesAsync()) + await foreach (var storable in homeFolder.GetLogicalDrivesAsync()) { - e.Flyout.Items.Add(new MenuFlyoutItem() { Text = storable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI) }); - storable.Dispose(); + if (storable is not IWindowsStorable windowsStorable) + continue; + + var flyoutItem = new MenuFlyoutItem() + { + Text = windowsStorable.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_PARENTRELATIVEFORUI), + Icon = new FontIcon { Glyph = "\uE8B7" }, // Use font icon as placeholder + }; + + e.Flyout.Items.Add(flyoutItem); + + windowsStorable.TryGetThumbnail((int)(16f * App.AppModel.AppWindowDPI), Windows.Win32.UI.Shell.SIIGBF.SIIGBF_ICONONLY, out var thumbnailData); + flyoutItem.Icon = new ImageIcon() { Source = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => thumbnailData.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal) }; + + windowsStorable.Dispose(); } return; diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs index 4b588a54167a..6b017e89c3c1 100644 --- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs @@ -839,7 +839,6 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p Icon = new FontIcon { Glyph = "\uE7BA" }, Text = Strings.SubDirectoryAccessDenied.GetLocalizedResource(), //Foreground = (SolidColorBrush)Application.Current.Resources["SystemControlErrorTextForegroundBrush"], - FontSize = 12 }; flyout.Items?.Add(flyoutItem); @@ -859,7 +858,6 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p { Icon = new FontIcon { Glyph = "\uE8B7" }, // Use font icon as placeholder Text = childFolder.Item.Name, - FontSize = 12, }; if (workingPath != childFolder.Path)