Skip to content

Commit cfabfc9

Browse files
Feature: Added option to create new shortcuts (#10879)
1 parent 67c7f7e commit cfabfc9

16 files changed

+296
-7
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<ContentDialog
2+
x:Class="Files.App.Dialogs.CreateShortcutDialog"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:helpers="using:Files.App.Helpers"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
Title="{helpers:ResourceString Name=NewShortcutDialogTitle}"
9+
DefaultButton="Primary"
10+
PrimaryButtonCommand="{x:Bind ViewModel.PrimaryButtonCommand}"
11+
IsPrimaryButtonEnabled="{x:Bind ViewModel.IsLocationValid, Mode=OneWay}"
12+
PrimaryButtonText="{helpers:ResourceString Name=Create}"
13+
RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}"
14+
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
15+
Style="{StaticResource DefaultContentDialogStyle}"
16+
mc:Ignorable="d">
17+
18+
<Border Width="400">
19+
<Grid
20+
x:Name="DestinationPathGrid"
21+
ColumnSpacing="8"
22+
RowSpacing="8">
23+
<Grid.ColumnDefinitions>
24+
<ColumnDefinition />
25+
<ColumnDefinition Width="Auto" />
26+
</Grid.ColumnDefinitions>
27+
<Grid.RowDefinitions>
28+
<RowDefinition Height="Auto" />
29+
<RowDefinition Height="Auto" />
30+
<RowDefinition Height="Auto" />
31+
</Grid.RowDefinitions>
32+
33+
<!-- Header -->
34+
<TextBlock
35+
Grid.ColumnSpan="2"
36+
Margin="0, 0, 0, 20"
37+
Text="{helpers:ResourceString Name=NewShortcutDialogDescription}"
38+
TextWrapping="Wrap" />
39+
40+
<TextBlock
41+
Grid.Row="1"
42+
Grid.ColumnSpan="2"
43+
Text="{helpers:ResourceString Name=NewShortcutDialogPrompt}" />
44+
45+
<!-- Path Box -->
46+
<TextBox
47+
x:Name="DestinationItemPath"
48+
Grid.Row="2"
49+
Grid.Column="0"
50+
HorizontalAlignment="Stretch"
51+
PlaceholderText="C:\Users\"
52+
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay}"
53+
TextChanged="DestinationItemPath_TextChanged" />
54+
<Button
55+
x:Name="SelectDestination"
56+
Grid.Row="2"
57+
Grid.Column="1"
58+
Command="{x:Bind ViewModel.SelectDestinationCommand}"
59+
Content="{helpers:ResourceString Name=Browse}" />
60+
</Grid>
61+
</Border>
62+
</ContentDialog>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using Files.App.ViewModels.Dialogs;
2+
using Files.Backend.ViewModels.Dialogs;
3+
using Files.Shared.Enums;
4+
using Microsoft.UI.Xaml.Controls;
5+
using System;
6+
using System.IO;
7+
using System.Threading.Tasks;
8+
9+
namespace Files.App.Dialogs
10+
{
11+
public sealed partial class CreateShortcutDialog : ContentDialog, IDialog<CreateShortcutDialogViewModel>
12+
{
13+
public CreateShortcutDialogViewModel ViewModel
14+
{
15+
get => (CreateShortcutDialogViewModel)DataContext;
16+
set => DataContext = value;
17+
}
18+
19+
public CreateShortcutDialog()
20+
{
21+
this.InitializeComponent();
22+
}
23+
24+
public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();
25+
26+
private void DestinationItemPath_TextChanged(object sender, TextChangedEventArgs e)
27+
{
28+
if (string.IsNullOrWhiteSpace(DestinationItemPath.Text))
29+
{
30+
ViewModel.IsLocationValid = false;
31+
return;
32+
}
33+
34+
try
35+
{
36+
ViewModel.DestinationPathExists = Path.Exists(DestinationItemPath.Text) && DestinationItemPath.Text != Path.GetPathRoot(DestinationItemPath.Text);
37+
if (ViewModel.DestinationPathExists)
38+
{
39+
ViewModel.IsLocationValid = true;
40+
}
41+
else
42+
{
43+
var uri = new Uri(DestinationItemPath.Text);
44+
ViewModel.IsLocationValid = uri.IsWellFormedOriginalString();
45+
}
46+
}
47+
catch (Exception)
48+
{
49+
ViewModel.IsLocationValid = false;
50+
}
51+
}
52+
}
53+
}

src/Files.App/Helpers/ContextFlyoutItemHelper.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,13 @@ public static List<ContextMenuFlyoutItemViewModel> GetNewItemItems(BaseLayoutCom
11091109
ShowInZipPage = true,
11101110
IsEnabled = canCreateFileInPage
11111111
},
1112+
new ContextMenuFlyoutItemViewModel
1113+
{
1114+
Text = "Shortcut".GetLocalizedResource(),
1115+
Glyph = "\uF10A",
1116+
GlyphFontFamilyName = "CustomGlyph",
1117+
Command = commandsViewModel.CreateShortcutFromDialogCommand
1118+
},
11121119
new ContextMenuFlyoutItemViewModel()
11131120
{
11141121
ItemType = ItemType.Separator,

src/Files.App/Helpers/UIFilesystemHelpers.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
using CommunityToolkit.Mvvm.DependencyInjection;
12
using Files.App.Dialogs;
23
using Files.App.Extensions;
34
using Files.App.Filesystem;
45
using Files.App.Filesystem.StorageItems;
56
using Files.App.Interacts;
67
using Files.App.ViewModels;
8+
using Files.App.ViewModels.Dialogs;
79
using Files.Backend.Enums;
10+
using Files.Backend.Services;
811
using Files.Shared;
912
using Files.Shared.Enums;
1013
using Files.Shared.Extensions;
@@ -386,5 +389,12 @@ public static void SetHiddenAttributeItem(ListedItem item, bool isHidden, ItemMa
386389
item.IsHiddenItem = isHidden;
387390
itemManipulationModel.RefreshItemsOpacity();
388391
}
392+
393+
public static async Task CreateShortcutFromDialogAsync(IShellPage associatedInstance)
394+
{
395+
var viewModel = new CreateShortcutDialogViewModel(associatedInstance.FilesystemViewModel.WorkingDirectory);
396+
var dialogService = Ioc.Default.GetRequiredService<IDialogService>();
397+
await dialogService.ShowDialogAsync(viewModel);
398+
}
389399
}
390400
}

src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public virtual async void CreateShortcut(RoutedEventArgs e)
9393
}
9494
}
9595

96+
public virtual async void CreateShortcutFromDialog(RoutedEventArgs e)
97+
{
98+
await UIFilesystemHelpers.CreateShortcutFromDialogAsync(associatedInstance);
99+
}
100+
96101
public virtual void SetAsLockscreenBackgroundItem(RoutedEventArgs e)
97102
{
98103
WallpaperHelpers.SetAsBackground(WallpaperType.LockScreen, SlimContentPage.SelectedItem.ItemPath);

src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ private void InitializeCommands()
2828
{
2929
RenameItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.RenameItem);
3030
CreateShortcutCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.CreateShortcut);
31+
CreateShortcutFromDialogCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.CreateShortcutFromDialog);
3132
SetAsLockscreenBackgroundItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsLockscreenBackgroundItem);
3233
SetAsDesktopBackgroundItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsDesktopBackgroundItem);
3334
SetAsSlideshowItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsSlideshowItem);
@@ -90,6 +91,8 @@ private void InitializeCommands()
9091

9192
public ICommand CreateShortcutCommand { get; private set; }
9293

94+
public ICommand CreateShortcutFromDialogCommand { get; private set; }
95+
9396
public ICommand SetAsLockscreenBackgroundItemCommand { get; private set; }
9497

9598
public ICommand SetAsDesktopBackgroundItemCommand { get; private set; }

src/Files.App/Interacts/IBaseLayoutCommandImplementationModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface IBaseLayoutCommandImplementationModel : IDisposable
1212

1313
void CreateShortcut(RoutedEventArgs e);
1414

15+
void CreateShortcutFromDialog(RoutedEventArgs e);
16+
1517
void SetAsLockscreenBackgroundItem(RoutedEventArgs e);
1618

1719
void SetAsDesktopBackgroundItem(RoutedEventArgs e);

src/Files.App/ServicesImplementation/DialogService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public DialogService()
2828
{ typeof(ElevateConfirmDialogViewModel), () => new ElevateConfirmDialog() },
2929
{ typeof(FileSystemDialogViewModel), () => new FilesystemOperationDialog() },
3030
{ typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() },
31-
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() }
31+
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() },
32+
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() }
3233
};
3334
}
3435

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2866,6 +2866,18 @@
28662866
<data name="SortBy" xml:space="preserve">
28672867
<value>Sort by</value>
28682868
</data>
2869+
<data name="NewShortcutDialogTitle" xml:space="preserve">
2870+
<value>Create a new shortcut</value>
2871+
</data>
2872+
<data name="NewShortcutDialogDescription" xml:space="preserve">
2873+
<value>Create shortcuts to local or network programs, files, folders, computers or Internet addresses.</value>
2874+
</data>
2875+
<data name="NewShortcutDialogPrompt" xml:space="preserve">
2876+
<value>Enter the location of the item:</value>
2877+
</data>
2878+
<data name="AddDialogListShortcutSubHeader" xml:space="preserve">
2879+
<value>Creates a shortcut</value>
2880+
</data>
28692881
<data name="RecentFilesDisabledOnWindowsWarning" xml:space="preserve">
28702882
<value>Recently used files is currently disabled in Windows File Explorer.</value>
28712883
</data>

src/Files.App/UserControls/InnerNavigationToolbar.xaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
<FontIcon Glyph="&#xE7C3;" />
8686
</MenuFlyoutItem.Icon>
8787
</MenuFlyoutItem>
88+
<MenuFlyoutItem
89+
x:Name="NewShortcut"
90+
AutomationProperties.AutomationId="InnerNavigationToolbarNewShortcutButton"
91+
Command="{x:Bind ViewModel.CreateNewShortcutCommand, Mode=OneWay}"
92+
Text="{helpers:ResourceString Name=Shortcut}">
93+
<MenuFlyoutItem.Icon>
94+
<FontIcon Glyph="&#xE71B;" />
95+
</MenuFlyoutItem.Icon>
96+
</MenuFlyoutItem>
8897
<MenuFlyoutSeparator x:Name="NewMenuFileFolderSeparator" />
8998
</MenuFlyout>
9099
</AppBarButton.Flyout>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
using CommunityToolkit.Mvvm.Input;
3+
using Files.App.Helpers;
4+
using Files.Backend.Extensions;
5+
using System;
6+
using System.IO;
7+
using System.Threading.Tasks;
8+
using System.Windows.Input;
9+
using Windows.Storage.Pickers;
10+
11+
namespace Files.App.ViewModels.Dialogs
12+
{
13+
public class CreateShortcutDialogViewModel : ObservableObject
14+
{
15+
// User's working directory
16+
public readonly string WorkingDirectory;
17+
18+
// Tells whether destination path exists
19+
public bool DestinationPathExists { get; set; } = false;
20+
21+
// Destination of the shortcut chosen by the user (can be a path or a URL)
22+
private string _destinationItemPath;
23+
public string DestinationItemPath
24+
{
25+
get => _destinationItemPath;
26+
set => SetProperty(ref _destinationItemPath, value);
27+
}
28+
29+
// Tells if the selected destination is valid (Path exists or URL is well-formed). Used to enable primary button
30+
private bool _isLocationValid;
31+
public bool IsLocationValid
32+
{
33+
get => _isLocationValid;
34+
set => SetProperty(ref _isLocationValid, value);
35+
}
36+
37+
// Command invoked when the user clicks the 'Browse' button
38+
public ICommand SelectDestinationCommand { get; private set; }
39+
40+
// Command invoked when the user clicks primary button
41+
public ICommand PrimaryButtonCommand { get; private set; }
42+
43+
public CreateShortcutDialogViewModel(string workingDirectory)
44+
{
45+
WorkingDirectory = workingDirectory;
46+
_destinationItemPath = string.Empty;
47+
48+
SelectDestinationCommand = new AsyncRelayCommand(SelectDestination);
49+
PrimaryButtonCommand = new AsyncRelayCommand(CreateShortcut);
50+
}
51+
52+
private async Task SelectDestination()
53+
{
54+
var folderPicker = InitializeWithWindow(new FolderPicker());
55+
folderPicker.FileTypeFilter.Add("*");
56+
57+
var selectedFolder = await folderPicker.PickSingleFolderAsync();
58+
if (selectedFolder is not null)
59+
DestinationItemPath = selectedFolder.Path;
60+
}
61+
62+
private FolderPicker InitializeWithWindow(FolderPicker obj)
63+
{
64+
WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle);
65+
return obj;
66+
}
67+
68+
private async Task CreateShortcut()
69+
{
70+
string? destinationName;
71+
var extension = DestinationPathExists ? ".lnk" : ".url";
72+
73+
if (DestinationPathExists)
74+
{
75+
destinationName = Path.GetFileName(DestinationItemPath);
76+
destinationName ??= Path.GetDirectoryName(DestinationItemPath);
77+
}
78+
else
79+
{
80+
var uri = new Uri(DestinationItemPath);
81+
destinationName = uri.Host;
82+
}
83+
84+
var shortcutName = string.Format("ShortcutCreateNewSuffix".ToLocalized(), destinationName);
85+
var filePath = Path.Combine(
86+
WorkingDirectory,
87+
shortcutName + extension);
88+
89+
int fileNumber = 1;
90+
while (Path.Exists(filePath))
91+
{
92+
filePath = Path.Combine(
93+
WorkingDirectory,
94+
shortcutName + $" ({++fileNumber})" + extension);
95+
}
96+
97+
await FileOperationsHelpers.CreateOrUpdateLinkAsync(filePath, DestinationItemPath);
98+
}
99+
}
100+
}

src/Files.App/ViewModels/ToolbarViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,8 @@ public void SearchRegion_LostFocus(object sender, RoutedEventArgs e)
791791

792792
public ICommand? CreateNewFolderCommand { get; set; }
793793

794+
public ICommand? CreateNewShortcutCommand { get; set; }
795+
794796
public ICommand? CopyCommand { get; set; }
795797

796798
public ICommand? DeleteCommand { get; set; }

src/Files.App/Views/ColumnShellPage.xaml.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ private void InitToolbarCommands()
275275
ToolbarViewModel.ClosePaneCommand = new RelayCommand(() => PaneHolder?.CloseActivePane());
276276
ToolbarViewModel.CreateNewFileCommand = new RelayCommand<ShellNewEntry>(x => UIFilesystemHelpers.CreateFileFromDialogResultType(AddItemDialogItemType.File, x, this));
277277
ToolbarViewModel.CreateNewFolderCommand = new RelayCommand(() => UIFilesystemHelpers.CreateFileFromDialogResultType(AddItemDialogItemType.Folder, null, this));
278+
ToolbarViewModel.CreateNewShortcutCommand = new RelayCommand(() => CreateNewShortcutFromDialog());
278279
ToolbarViewModel.CopyCommand = new RelayCommand(async () => await UIFilesystemHelpers.CopyItem(this));
279280
ToolbarViewModel.Rename = new RelayCommand(() => SlimContentPage?.CommandsViewModel.RenameItemCommand.Execute(null));
280281
ToolbarViewModel.Share = new RelayCommand(() => SlimContentPage?.CommandsViewModel.ShareItemCommand.Execute(null));
@@ -689,13 +690,13 @@ private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, Keybo
689690
{
690691
var addItemDialogViewModel = new AddItemDialogViewModel();
691692
await DialogService.ShowDialogAsync(addItemDialogViewModel);
692-
if (addItemDialogViewModel.ResultType.ItemType != AddItemDialogItemType.Cancel)
693-
{
693+
if (addItemDialogViewModel.ResultType.ItemType == AddItemDialogItemType.Shortcut)
694+
CreateNewShortcutFromDialog();
695+
else if (addItemDialogViewModel.ResultType.ItemType != AddItemDialogItemType.Cancel)
694696
UIFilesystemHelpers.CreateFileFromDialogResultType(
695697
addItemDialogViewModel.ResultType.ItemType,
696698
addItemDialogViewModel.ResultType.ItemInfo,
697699
this);
698-
}
699700
}
700701
break;
701702

@@ -1045,5 +1046,8 @@ public void SubmitSearch(string query, bool searchUnindexedItems)
10451046
});
10461047
//this.FindAscendant<ColumnViewBrowser>().SetSelectedPathOrNavigate(null, typeof(ColumnViewBase), navArgs);
10471048
}
1049+
1050+
private async void CreateNewShortcutFromDialog()
1051+
=> await UIFilesystemHelpers.CreateShortcutFromDialogAsync(this);
10481052
}
10491053
}

0 commit comments

Comments
 (0)