Skip to content

Commit 9ae6397

Browse files
authored
Feature: Added navigation history flyout when right clicking the back/forward buttons (#15873)
1 parent 1074a93 commit 9ae6397

File tree

7 files changed

+188
-14
lines changed

7 files changed

+188
-14
lines changed

src/Files.App/Data/Contracts/IShellPage.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using Microsoft.UI.Xaml.Navigation;
5+
46
namespace Files.App.Data.Contracts
57
{
68
public interface IShellPage : ITabBarItemContent, IMultiPaneInfo, IDisposable, INotifyPropertyChanged
@@ -11,6 +13,10 @@ public interface IShellPage : ITabBarItemContent, IMultiPaneInfo, IDisposable, I
1113

1214
StorageHistoryHelpers StorageHistoryHelpers { get; }
1315

16+
IList<PageStackEntry> ForwardStack { get; }
17+
18+
IList<PageStackEntry> BackwardStack { get; }
19+
1420
IBaseLayoutPage SlimContentPage { get; }
1521

1622
Type CurrentPageType { get; }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.UI.Xaml.Navigation;
5+
6+
namespace Files.App.Data.Models
7+
{
8+
internal sealed class ToolbarHistoryItemModel
9+
{
10+
public PageStackEntry PageStackEntry { get; }
11+
12+
public bool IsBackMode { get; }
13+
14+
public ToolbarHistoryItemModel(PageStackEntry pageStackEntry, bool isBackMode)
15+
{
16+
PageStackEntry = pageStackEntry;
17+
IsBackMode = isBackMode;
18+
}
19+
}
20+
}

src/Files.App/Helpers/Navigation/NavigationHelpers.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Windows.Storage;
88
using Windows.Storage.Search;
99
using Windows.System;
10+
using Microsoft.UI.Xaml.Media;
1011

1112
namespace Files.App.Helpers
1213
{
@@ -130,6 +131,35 @@ private static async Task UpdateTabInfoAsync(TabBarItem tabItem, object navigati
130131
}
131132
}
132133

134+
public static async Task<ImageSource?> GetIconForPathAsync(string path)
135+
{
136+
ImageSource? imageSource;
137+
if (string.IsNullOrEmpty(path) || path == "Home")
138+
imageSource = new BitmapImage(new Uri(Constants.FluentIconsPaths.HomeIcon));
139+
else if (WSLDistroManager.TryGetDistro(path, out WslDistroItem? wslDistro) && path.Equals(wslDistro.Path))
140+
imageSource = new BitmapImage(wslDistro.Icon);
141+
else
142+
{
143+
var normalizedPath = PathNormalization.NormalizePath(path);
144+
var matchingCloudDrive = CloudDrivesManager.Drives.FirstOrDefault(x => normalizedPath.Equals(PathNormalization.NormalizePath(x.Path), StringComparison.OrdinalIgnoreCase));
145+
imageSource = matchingCloudDrive?.Icon;
146+
147+
if (imageSource is null)
148+
{
149+
var result = await FileThumbnailHelper.GetIconAsync(
150+
path,
151+
Constants.ShellIconSizes.Small,
152+
true,
153+
IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale);
154+
155+
if (result is not null)
156+
imageSource = await result.ToBitmapAsync();
157+
}
158+
}
159+
160+
return imageSource;
161+
}
162+
133163
public static async Task<(string tabLocationHeader, IconSource tabIcon, string toolTipText)> GetSelectedTabInfoAsync(string currentPath)
134164
{
135165
string? tabLocationHeader;

src/Files.App/UserControls/AddressToolbar.xaml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
66
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
77
xmlns:contract8Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,8)"
8-
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
98
xmlns:converters="using:Files.App.Converters"
109
xmlns:converters1="using:CommunityToolkit.WinUI.UI.Converters"
1110
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
1211
xmlns:extensions="using:CommunityToolkit.WinUI.UI"
1312
xmlns:helpers="using:Files.App.Helpers"
1413
xmlns:items="using:Files.App.Data.Items"
1514
xmlns:keyboard="using:Files.App.UserControls.KeyboardShortcut"
15+
xmlns:local="using:Files.App.UserControls"
1616
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
1717
xmlns:triggers="using:CommunityToolkit.WinUI.UI.Triggers"
1818
xmlns:uc="using:Files.App.UserControls"
1919
xmlns:ucs="using:Files.App.UserControls.StatusCenter"
20+
xmlns:vm="using:Files.App.ViewModels.UserControls"
2021
x:Name="NavToolbar"
2122
Height="50"
2223
d:DesignHeight="50"
@@ -237,7 +238,7 @@
237238

238239
<!-- Page Navigation Actions -->
239240
<StackPanel
240-
Grid.Row="1"
241+
Grid.Column="0"
241242
Orientation="Horizontal"
242243
Spacing="4">
243244

@@ -274,6 +275,22 @@
274275
Style="{StaticResource AddressToolbarButtonStyle}"
275276
ToolTipService.ToolTip="{x:Bind ViewModel.Commands.NavigateBack.LabelWithHotKey, Mode=OneWay}">
276277
<FontIcon FontSize="14" Glyph="{x:Bind ViewModel.Commands.NavigateBack.Glyph.BaseGlyph, Mode=OneWay}" />
278+
<Button.ContextFlyout>
279+
<MenuFlyout
280+
x:Name="BackHistoryFlyout"
281+
Opening="BackHistoryFlyout_Opening"
282+
Placement="BottomEdgeAlignedLeft"
283+
ScrollViewer.VerticalScrollBarVisibility="Auto"
284+
ScrollViewer.VerticalScrollMode="Auto">
285+
<MenuFlyout.MenuFlyoutPresenterStyle>
286+
<Style TargetType="MenuFlyoutPresenter">
287+
<Setter Property="MaxHeight" Value="400" />
288+
<!-- Workaround for https://github.com/files-community/Files/issues/13078 -->
289+
<Setter Target="HighContrastAdjustment" Value="None" />
290+
</Style>
291+
</MenuFlyout.MenuFlyoutPresenterStyle>
292+
</MenuFlyout>
293+
</Button.ContextFlyout>
277294
</Button>
278295

279296
<Button
@@ -288,6 +305,22 @@
288305
Style="{StaticResource AddressToolbarButtonStyle}"
289306
ToolTipService.ToolTip="{x:Bind ViewModel.Commands.NavigateForward.LabelWithHotKey, Mode=OneWay}">
290307
<FontIcon FontSize="14" Glyph="{x:Bind ViewModel.Commands.NavigateForward.Glyph.BaseGlyph, Mode=OneWay}" />
308+
<Button.ContextFlyout>
309+
<MenuFlyout
310+
x:Name="ForwardHistoryFlyout"
311+
Opening="ForwardHistoryFlyout_Opening"
312+
Placement="BottomEdgeAlignedLeft"
313+
ScrollViewer.VerticalScrollBarVisibility="Auto"
314+
ScrollViewer.VerticalScrollMode="Auto">
315+
<MenuFlyout.MenuFlyoutPresenterStyle>
316+
<Style TargetType="MenuFlyoutPresenter">
317+
<Setter Property="MaxHeight" Value="400" />
318+
<!-- Workaround for https://github.com/files-community/Files/issues/13078 -->
319+
<Setter Target="HighContrastAdjustment" Value="None" />
320+
</Style>
321+
</MenuFlyout.MenuFlyoutPresenterStyle>
322+
</MenuFlyout>
323+
</Button.ContextFlyout>
291324
</Button>
292325

293326
<Button
@@ -367,7 +400,6 @@
367400
x:Name="RightAlignedKeyboardShortcut"
368401
Grid.Column="1"
369402
HotKeys="{x:Bind HotKeys, Mode=OneWay}" />
370-
371403
</Grid>
372404
</StackPanel>
373405
</DataTemplate>

src/Files.App/UserControls/AddressToolbar.xaml.cs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using System.Windows.Input;
45
using Microsoft.UI.Input;
56
using Microsoft.UI.Xaml;
67
using Microsoft.UI.Xaml.Controls;
78
using Microsoft.UI.Xaml.Controls.Primitives;
89
using Microsoft.UI.Xaml.Input;
910
using Microsoft.UI.Xaml.Media;
1011
using Windows.System;
12+
using Microsoft.UI.Xaml.Navigation;
1113
using FocusManager = Microsoft.UI.Xaml.Input.FocusManager;
1214

1315
namespace Files.App.UserControls
1416
{
1517
public sealed partial class AddressToolbar : UserControl
1618
{
1719
private readonly IUserSettingsService userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
20+
private readonly ICommand historyItemClickedCommand;
1821
private readonly MainPageViewModel MainPageViewModel = Ioc.Default.GetRequiredService<MainPageViewModel>();
22+
1923
public ICommandManager Commands { get; } = Ioc.Default.GetRequiredService<ICommandManager>();
2024

2125
public static readonly DependencyProperty IsSidebarPaneOpenToggleButtonVisibleProperty =
@@ -64,7 +68,11 @@ public AddressToolbarViewModel? ViewModel
6468

6569
public StatusCenterViewModel? OngoingTasksViewModel { get; set; }
6670

67-
public AddressToolbar() => InitializeComponent();
71+
public AddressToolbar()
72+
{
73+
InitializeComponent();
74+
historyItemClickedCommand = new RelayCommand<ToolbarHistoryItemModel?>(HistoryItemClicked);
75+
}
6876

6977
private void NavToolbar_Loading(FrameworkElement _, object e)
7078
{
@@ -159,5 +167,87 @@ private void Button_AccessKeyInvoked(UIElement sender, AccessKeyInvokedEventArgs
159167
if (VisualTreeHelper.GetOpenPopupsForXamlRoot(MainWindow.Instance.Content.XamlRoot).Any())
160168
args.Handled = true;
161169
}
170+
171+
private async void BackHistoryFlyout_Opening(object? sender, object e)
172+
{
173+
var shellPage = Ioc.Default.GetRequiredService<IContentPageContext>().ShellPage;
174+
if (shellPage is null)
175+
return;
176+
177+
await AddHistoryItemsAsync(shellPage.BackwardStack, BackHistoryFlyout.Items, true);
178+
}
179+
180+
private async void ForwardHistoryFlyout_Opening(object? sender, object e)
181+
{
182+
var shellPage = Ioc.Default.GetRequiredService<IContentPageContext>().ShellPage;
183+
if (shellPage is null)
184+
return;
185+
186+
await AddHistoryItemsAsync(shellPage.ForwardStack, ForwardHistoryFlyout.Items, false);
187+
}
188+
189+
private async Task AddHistoryItemsAsync(IEnumerable<PageStackEntry> items, IList<MenuFlyoutItemBase> destination, bool isBackMode)
190+
{
191+
// This may not seem performant, however it's the most viable trade-off to make.
192+
// Instead of constantly keeping track of back/forward stack and performing lookups
193+
// (which may degrade performance), we only add items in bulk when it's needed.
194+
// There's also a high chance the user might not use the feature at all in which case
195+
// the former approach would just waste extra performance gain
196+
197+
destination.Clear();
198+
foreach (var item in items.Reverse())
199+
{
200+
if (item.Parameter is not NavigationArguments args || args.NavPathParam is null)
201+
continue;
202+
203+
var imageSource = await NavigationHelpers.GetIconForPathAsync(args.NavPathParam);
204+
var fileName = SystemIO.Path.GetFileName(args.NavPathParam);
205+
206+
// The fileName is empty if the path is (root) drive path
207+
if (string.IsNullOrEmpty(fileName))
208+
fileName = args.NavPathParam;
209+
210+
destination.Add(new MenuFlyoutItem()
211+
{
212+
Icon = new ImageIcon() { Source = imageSource },
213+
Text = fileName,
214+
Command = historyItemClickedCommand,
215+
CommandParameter = new ToolbarHistoryItemModel(item, isBackMode)
216+
});
217+
}
218+
}
219+
220+
private void HistoryItemClicked(ToolbarHistoryItemModel? itemModel)
221+
{
222+
if (itemModel is null)
223+
return;
224+
225+
var shellPage = Ioc.Default.GetRequiredService<IContentPageContext>().ShellPage;
226+
if (shellPage is null)
227+
return;
228+
229+
if (itemModel.IsBackMode)
230+
{
231+
// Remove all entries after the target entry in the BackwardStack
232+
while (shellPage.BackwardStack.Last() != itemModel.PageStackEntry)
233+
{
234+
shellPage.BackwardStack.RemoveAt(shellPage.BackwardStack.Count - 1);
235+
}
236+
237+
// Navigate back
238+
shellPage.Back_Click();
239+
}
240+
else
241+
{
242+
// Remove all entries before the target entry in the ForwardStack
243+
while (shellPage.ForwardStack.First() != itemModel.PageStackEntry)
244+
{
245+
shellPage.ForwardStack.RemoveAt(0);
246+
}
247+
248+
// Navigate forward
249+
shellPage.Forward_Click();
250+
}
251+
}
162252
}
163253
}

src/Files.App/Views/Shells/BaseShellPage.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Microsoft.UI.Xaml;
66
using Microsoft.UI.Xaml.Controls;
77
using Microsoft.UI.Xaml.Input;
8-
using Microsoft.UI.Xaml.Media;
98
using Microsoft.UI.Xaml.Media.Animation;
109
using Microsoft.UI.Xaml.Navigation;
1110
using System.Runtime.CompilerServices;
@@ -62,12 +61,16 @@ public abstract class BaseShellPage : Page, IShellPage, INotifyPropertyChanged
6261

6362
protected abstract Frame ItemDisplay { get; }
6463

65-
public abstract bool CanNavigateForward { get; }
64+
public virtual bool CanNavigateForward => ItemDisplay.CanGoForward;
6665

67-
public abstract bool CanNavigateBackward { get; }
66+
public virtual bool CanNavigateBackward => ItemDisplay.CanGoBack;
6867

6968
public bool IsColumnView => SlimContentPage is ColumnsLayoutPage;
7069

70+
public virtual IList<PageStackEntry> ForwardStack => ItemDisplay.ForwardStack;
71+
72+
public virtual IList<PageStackEntry> BackwardStack => ItemDisplay.BackStack;
73+
7174
public ShellViewModel ShellViewModel { get; protected set; }
7275

7376
public CurrentInstanceViewModel InstanceViewModel { get; }

src/Files.App/Views/Shells/ModernShellPage.xaml.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using Microsoft.UI.Xaml;
54
using Microsoft.UI.Xaml.Controls;
65
using Microsoft.UI.Xaml.Input;
76
using Microsoft.UI.Xaml.Media.Animation;
@@ -13,12 +12,6 @@ namespace Files.App.Views.Shells
1312
{
1413
public sealed partial class ModernShellPage : BaseShellPage
1514
{
16-
public override bool CanNavigateBackward
17-
=> ItemDisplayFrame.CanGoBack;
18-
19-
public override bool CanNavigateForward
20-
=> ItemDisplayFrame.CanGoForward;
21-
2215
protected override Frame ItemDisplay
2316
=> ItemDisplayFrame;
2417

0 commit comments

Comments
 (0)