Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Files.App/UserControls/TreeViewSidebar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,7 +13,7 @@
mc:Ignorable="d">

<UserControl.Resources>
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</UserControl.Resources>

<controls:TreeView
Expand Down
80 changes: 72 additions & 8 deletions src/Files.App/UserControls/TreeViewSidebar.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e)
_mainPageViewModel.Value.PropertyChanged += OnMainPageViewModelPropertyChanged;
if (_sidebarViewModel.Value.SidebarItems is INotifyCollectionChanged inc)
inc.CollectionChanged += OnSidebarItemsChanged;
MainPageViewModel.AppInstances.CollectionChanged += OnAppInstancesChanged;
_currentTab = _mainPageViewModel.Value.SelectedTabItem;
RebuildAndApply();
}
Expand All @@ -73,6 +74,7 @@ private void UserControl_Unloaded(object sender, RoutedEventArgs e)
_mainPageViewModel.Value.PropertyChanged -= OnMainPageViewModelPropertyChanged;
if (_sidebarViewModel.IsValueCreated && _sidebarViewModel.Value.SidebarItems is INotifyCollectionChanged inc)
inc.CollectionChanged -= OnSidebarItemsChanged;
MainPageViewModel.AppInstances.CollectionChanged -= OnAppInstancesChanged;
DetachAllNodeHandlers();
}
// Cleanup must never throw; the page is being torn down
Expand Down Expand Up @@ -127,12 +129,12 @@ private void Rebuild()
DriveItem drv => 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);
}
Expand Down Expand Up @@ -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<TabBarItem>())
_tabExpansionState.Remove(item);
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
_tabExpansionState.Clear();
}
}

private void OnMainPageViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(MainPageViewModel.SelectedTabItem))
Expand Down Expand Up @@ -250,21 +265,59 @@ 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)
{
App.Logger?.LogWarning(ex, "TreeViewSidebar: lazy expand failed");
}
}

// 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)
Expand Down Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -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);

Expand Down
22 changes: 8 additions & 14 deletions src/Files.App/Views/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Loading