diff --git a/src/Files.App/UserControls/NullToVisibilityConverter.cs b/src/Files.App/Converters/NullToVisibilityConverter.cs
similarity index 80%
rename from src/Files.App/UserControls/NullToVisibilityConverter.cs
rename to src/Files.App/Converters/NullToVisibilityConverter.cs
index 1c5f022089d6..49059527ea96 100644
--- a/src/Files.App/UserControls/NullToVisibilityConverter.cs
+++ b/src/Files.App/Converters/NullToVisibilityConverter.cs
@@ -4,9 +4,9 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
-namespace Files.App.UserControls
+namespace Files.App.Converters
{
- public sealed partial class NullToVisibilityConverter : IValueConverter
+ internal sealed partial class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
=> value is null ? Visibility.Collapsed : Visibility.Visible;
diff --git a/src/Files.App/UserControls/TreeViewSidebar.xaml b/src/Files.App/UserControls/TreeViewSidebar.xaml
index 6386258744fd..761f5d1c388a 100644
--- a/src/Files.App/UserControls/TreeViewSidebar.xaml
+++ b/src/Files.App/UserControls/TreeViewSidebar.xaml
@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
+ xmlns:converters="using:Files.App.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Files.App.UserControls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,7 +13,7 @@
mc:Ignorable="d">
-
+
drv.Icon,
_ => null,
};
- var hasRootedPath = (child is LocationItem loc2 && System.IO.Path.IsPathRooted(loc2.Path)) || child is DriveItem;
+ var hasRootedPath = (child is LocationItem loc2 && Path.IsPathRooted(loc2.Path)) || child is DriveItem;
var isExpandable = hasRootedPath && header.Section != SectionType.Pinned;
var kind = isExpandable ? FolderNodeKind.Folder : FolderNodeKind.Leaf;
var node = new FolderNode(path, name, kind, icon);
if (isExpandable)
- node.HasUnrealizedChildren = SafeHasSubdirectories(path);
+ _ = ProbeHasChildrenAsync(node);
node.PropertyChanged += OnNodePropertyChanged;
section.Children.Add(node);
}
@@ -222,6 +224,19 @@ private void OnSidebarItemsChanged(object? sender, NotifyCollectionChangedEventA
DispatcherQueue.TryEnqueue(RebuildAndApply);
}
+ private void OnAppInstancesChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems is not null)
+ {
+ foreach (var item in e.OldItems.OfType())
+ _tabExpansionState.Remove(item);
+ }
+ else if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ _tabExpansionState.Clear();
+ }
+ }
+
private void OnMainPageViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(MainPageViewModel.SelectedTabItem))
@@ -250,13 +265,13 @@ private void HandleTabChange()
}
}
- private void Tree_Expanding(TreeView sender, TreeViewExpandingEventArgs args)
+ private async void Tree_Expanding(TreeView sender, TreeViewExpandingEventArgs args)
{
try
{
if (args.Node.Content is not FolderNode fn)
return;
- LoadChildrenSync(fn);
+ await LoadChildrenAsync(fn);
}
catch (Exception ex)
{
@@ -264,7 +279,45 @@ private void Tree_Expanding(TreeView sender, TreeViewExpandingEventArgs args)
}
}
- // Synchronous lazy load. Called from Tree_Expanding (user click) and ApplyExpansionRecursive (state restore).
+ private async Task LoadChildrenAsync(FolderNode parent)
+ {
+ if (parent.Kind != FolderNodeKind.Folder || !parent.HasUnrealizedChildren)
+ return;
+
+ // Mark before awaiting so a stray re-entry doesn't double-load
+ parent.HasUnrealizedChildren = false;
+
+ var parentPath = parent.Path;
+ var loaded = await Task.Run(() =>
+ {
+ var results = new List<(string Sub, string Name, bool HasGrandchildren)>();
+ foreach (var sub in SafeEnumerateSubdirectories(parentPath))
+ {
+ var subName = Path.GetFileName(sub);
+ if (string.IsNullOrEmpty(subName))
+ continue;
+ results.Add((sub, subName, SafeHasSubdirectories(sub)));
+ }
+ return results;
+ });
+
+ if (_isUnloaded)
+ return;
+
+ foreach (var (sub, subName, hasGrandchildren) in loaded)
+ {
+ var kind = hasGrandchildren ? FolderNodeKind.Folder : FolderNodeKind.Leaf;
+ var child = new FolderNode(sub, subName, kind, icon: null)
+ {
+ HasUnrealizedChildren = hasGrandchildren,
+ };
+ child.PropertyChanged += OnNodePropertyChanged;
+ _ = LoadIconAsync(child);
+ parent.Children.Add(child);
+ }
+ }
+
+ // Synchronous lazy load. Called from ApplyExpansionRecursive (state restore) to keep tab-switch restoration synchronous.
private void LoadChildrenSync(FolderNode parent)
{
if (parent.Kind != FolderNodeKind.Folder || !parent.HasUnrealizedChildren)
@@ -308,6 +361,15 @@ private async Task LoadIconAsync(FolderNode node)
}
}
+ private async Task ProbeHasChildrenAsync(FolderNode node)
+ {
+ var path = node.Path;
+ var hasChildren = await Task.Run(() => SafeHasSubdirectories(path));
+ if (_isUnloaded)
+ return;
+ node.HasUnrealizedChildren = hasChildren;
+ }
+
private void Tree_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{
if (args.InvokedItem is not FolderNode fn)
@@ -328,15 +390,15 @@ private void Item_RightTapped(object sender, RightTappedRoutedEventArgs e)
var flyout = new MenuFlyout();
- var open = new MenuFlyoutItem { Text = "Open" };
+ var open = new MenuFlyoutItem { Text = Strings.Open.GetLocalizedResource() };
open.Click += (_, _) => _contentPageContext.Value.ShellPage?.NavigateToPath(fn.Path);
flyout.Items.Add(open);
- var openNewTab = new MenuFlyoutItem { Text = "Open in new tab" };
+ var openNewTab = new MenuFlyoutItem { Text = Strings.OpenInNewTab.GetLocalizedResource() };
openNewTab.Click += async (_, _) => await NavigationHelpers.OpenPathInNewTab(fn.Path, true);
flyout.Items.Add(openNewTab);
- var copyPath = new MenuFlyoutItem { Text = "Copy path" };
+ var copyPath = new MenuFlyoutItem { Text = Strings.CopyPath.GetLocalizedResource() };
copyPath.Click += (_, _) =>
{
var dp = new DataPackage();
@@ -353,6 +415,8 @@ private void Item_RightTapped(object sender, RightTappedRoutedEventArgs e)
// Win32Exception from Process.Start when the shell launch fails (invalid path or denied access)
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = fn.Path, UseShellExecute = true }); }
catch (System.ComponentModel.Win32Exception) { }
+ catch (InvalidOperationException) { }
+ catch (FileNotFoundException) { }
};
flyout.Items.Add(openExplorer);
diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs
index 14acec218e45..9dc54dafc7df 100644
--- a/src/Files.App/Views/MainPage.xaml.cs
+++ b/src/Files.App/Views/MainPage.xaml.cs
@@ -342,31 +342,25 @@ private void SidebarControl_Loaded(object sender, RoutedEventArgs e)
SidebarAdaptiveViewModel.UpdateTabControlMargin();
}
- public object? GetEffectiveMenuItemsSource(bool showTreeViewSidebar)
- {
- // When the experimental TreeView sidebar is enabled, the original menu items must be hidden so the TreeView (overlaid in SidebarContent) is the only thing visible in the pane.
- return showTreeViewSidebar ? null : SidebarAdaptiveViewModel.SidebarItems;
- }
-
// Width threshold below which the experimental TreeView yields the pane back to the original sidebar's compact (icons-only) mode.
public const double TreeViewSidebarMinWidth = 200d;
- public Visibility GetTreeViewSidebarVisibility(Files.App.Controls.SidebarDisplayMode displayMode, bool isPaneOpen, double sidebarWidth)
+ public Visibility GetTreeViewSidebarVisibility(SidebarDisplayMode displayMode, bool isPaneOpen, double sidebarWidth)
{
return displayMode switch
{
// Window is small → SidebarView is in flyout mode. TreeView only shows when the user opens the pane via hamburger.
- Files.App.Controls.SidebarDisplayMode.Minimal => isPaneOpen ? Visibility.Visible : Visibility.Collapsed,
+ SidebarDisplayMode.Minimal => isPaneOpen ? Visibility.Visible : Visibility.Collapsed,
// Pane was resized narrow → SidebarView falls back to its compact icons-only mode; hide the TreeView entirely.
- Files.App.Controls.SidebarDisplayMode.Compact => Visibility.Collapsed,
+ SidebarDisplayMode.Compact => Visibility.Collapsed,
// Expanded mode: only show TreeView while the pane is wider than the readable threshold.
- Files.App.Controls.SidebarDisplayMode.Expanded => sidebarWidth >= TreeViewSidebarMinWidth ? Visibility.Visible : Visibility.Collapsed,
+ SidebarDisplayMode.Expanded => sidebarWidth >= TreeViewSidebarMinWidth ? Visibility.Visible : Visibility.Collapsed,
_ => Visibility.Collapsed,
};
}
// When the experimental TreeView is enabled and the pane is wide enough, hide the original SidebarView's menu items so they don't double up. In all other cases keep the SidebarView's items visible (compact icons / flyout / etc).
- public object? GetEffectiveMenuItemsSourceResponsive(bool showTreeViewSidebar, Files.App.Controls.SidebarDisplayMode displayMode, bool isPaneOpen, double sidebarWidth)
+ public object? GetEffectiveMenuItemsSourceResponsive(bool showTreeViewSidebar, SidebarDisplayMode displayMode, bool isPaneOpen, double sidebarWidth)
{
if (!showTreeViewSidebar)
return SidebarAdaptiveViewModel.SidebarItems;
@@ -382,12 +376,12 @@ private void ApplyTreeViewSidebarCompactMode()
if (!DevToolsSettingsService.ShowTreeViewSidebar)
return;
// Window-driven Minimal state is owned by the VisualStateManager — don't fight it.
- if (SidebarAdaptiveViewModel.SidebarDisplayMode == Files.App.Controls.SidebarDisplayMode.Minimal)
+ if (SidebarAdaptiveViewModel.SidebarDisplayMode == SidebarDisplayMode.Minimal)
return;
var width = UserSettingsService.AppearanceSettingsService.SidebarWidth;
var desired = width < TreeViewSidebarMinWidth
- ? Files.App.Controls.SidebarDisplayMode.Compact
- : Files.App.Controls.SidebarDisplayMode.Expanded;
+ ? SidebarDisplayMode.Compact
+ : SidebarDisplayMode.Expanded;
if (SidebarAdaptiveViewModel.SidebarDisplayMode != desired)
SidebarAdaptiveViewModel.SidebarDisplayMode = desired;
}