Skip to content

Feature: Added option to create new shortcuts #10879

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
62 changes: 62 additions & 0 deletions src/Files.App/Dialogs/CreateShortcutDialog.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<ContentDialog
x:Class="Files.App.Dialogs.CreateShortcutDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{helpers:ResourceString Name=NewShortcutDialogTitle}"
DefaultButton="Primary"
PrimaryButtonCommand="{x:Bind ViewModel.PrimaryButtonCommand}"
IsPrimaryButtonEnabled="{x:Bind ViewModel.IsLocationValid, Mode=OneWay}"
PrimaryButtonText="{helpers:ResourceString Name=Create}"
RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}"
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">

<Border Width="400">
<Grid
x:Name="DestinationPathGrid"
ColumnSpacing="8"
RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<!-- Header -->
<TextBlock
Grid.ColumnSpan="2"
Margin="0, 0, 0, 20"
Text="{helpers:ResourceString Name=NewShortcutDialogDescription}"
TextWrapping="Wrap" />

<TextBlock
Grid.Row="1"
Grid.ColumnSpan="2"
Text="{helpers:ResourceString Name=NewShortcutDialogPrompt}" />

<!-- Path Box -->
<TextBox
x:Name="DestinationItemPath"
Grid.Row="2"
Grid.Column="0"
HorizontalAlignment="Stretch"
PlaceholderText="C:\Users\"
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay}"
TextChanged="DestinationItemPath_TextChanged" />
<Button
x:Name="SelectDestination"
Grid.Row="2"
Grid.Column="1"
Command="{x:Bind ViewModel.SelectDestinationCommand}"
Content="{helpers:ResourceString Name=Browse}" />
</Grid>
</Border>
</ContentDialog>
53 changes: 53 additions & 0 deletions src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Files.App.ViewModels.Dialogs;
using Files.Backend.ViewModels.Dialogs;
using Files.Shared.Enums;
using Microsoft.UI.Xaml.Controls;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Files.App.Dialogs
{
public sealed partial class CreateShortcutDialog : ContentDialog, IDialog<CreateShortcutDialogViewModel>
{
public CreateShortcutDialogViewModel ViewModel
{
get => (CreateShortcutDialogViewModel)DataContext;
set => DataContext = value;
}

public CreateShortcutDialog()
{
this.InitializeComponent();
}

public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();

private void DestinationItemPath_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(DestinationItemPath.Text))
{
ViewModel.IsLocationValid = false;
return;
}

try
{
ViewModel.DestinationPathExists = Path.Exists(DestinationItemPath.Text) && DestinationItemPath.Text != Path.GetPathRoot(DestinationItemPath.Text);
if (ViewModel.DestinationPathExists)
{
ViewModel.IsLocationValid = true;
}
else
{
var uri = new Uri(DestinationItemPath.Text);
ViewModel.IsLocationValid = uri.IsWellFormedOriginalString();
}
}
catch (Exception)
{
ViewModel.IsLocationValid = false;
}
}
}
}
7 changes: 7 additions & 0 deletions src/Files.App/Helpers/ContextFlyoutItemHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,13 @@ public static List<ContextMenuFlyoutItemViewModel> GetNewItemItems(BaseLayoutCom
ShowInZipPage = true,
IsEnabled = canCreateFileInPage
},
new ContextMenuFlyoutItemViewModel
{
Text = "Shortcut".GetLocalizedResource(),
Glyph = "\uF10A",
GlyphFontFamilyName = "CustomGlyph",
Command = commandsViewModel.CreateShortcutFromDialogCommand
},
new ContextMenuFlyoutItemViewModel()
{
ItemType = ItemType.Separator,
Expand Down
10 changes: 10 additions & 0 deletions src/Files.App/Helpers/UIFilesystemHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using CommunityToolkit.Mvvm.DependencyInjection;
using Files.App.Dialogs;
using Files.App.Extensions;
using Files.App.Filesystem;
using Files.App.Filesystem.StorageItems;
using Files.App.Interacts;
using Files.App.ViewModels;
using Files.App.ViewModels.Dialogs;
using Files.Backend.Enums;
using Files.Backend.Services;
using Files.Shared;
using Files.Shared.Enums;
using Files.Shared.Extensions;
Expand Down Expand Up @@ -386,5 +389,12 @@ public static void SetHiddenAttributeItem(ListedItem item, bool isHidden, ItemMa
item.IsHiddenItem = isHidden;
itemManipulationModel.RefreshItemsOpacity();
}

public static async Task CreateShortcutFromDialogAsync(IShellPage associatedInstance)
{
var viewModel = new CreateShortcutDialogViewModel(associatedInstance.FilesystemViewModel.WorkingDirectory);
var dialogService = Ioc.Default.GetRequiredService<IDialogService>();
await dialogService.ShowDialogAsync(viewModel);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public virtual async void CreateShortcut(RoutedEventArgs e)
}
}

public virtual async void CreateShortcutFromDialog(RoutedEventArgs e)
{
await UIFilesystemHelpers.CreateShortcutFromDialogAsync(associatedInstance);
}

public virtual void SetAsLockscreenBackgroundItem(RoutedEventArgs e)
{
WallpaperHelpers.SetAsBackground(WallpaperType.LockScreen, SlimContentPage.SelectedItem.ItemPath);
Expand Down
3 changes: 3 additions & 0 deletions src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private void InitializeCommands()
{
RenameItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.RenameItem);
CreateShortcutCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.CreateShortcut);
CreateShortcutFromDialogCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.CreateShortcutFromDialog);
SetAsLockscreenBackgroundItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsLockscreenBackgroundItem);
SetAsDesktopBackgroundItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsDesktopBackgroundItem);
SetAsSlideshowItemCommand = new RelayCommand<RoutedEventArgs>(CommandsModel.SetAsSlideshowItem);
Expand Down Expand Up @@ -90,6 +91,8 @@ private void InitializeCommands()

public ICommand CreateShortcutCommand { get; private set; }

public ICommand CreateShortcutFromDialogCommand { get; private set; }

public ICommand SetAsLockscreenBackgroundItemCommand { get; private set; }

public ICommand SetAsDesktopBackgroundItemCommand { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IBaseLayoutCommandImplementationModel : IDisposable

void CreateShortcut(RoutedEventArgs e);

void CreateShortcutFromDialog(RoutedEventArgs e);

void SetAsLockscreenBackgroundItem(RoutedEventArgs e);

void SetAsDesktopBackgroundItem(RoutedEventArgs e);
Expand Down
3 changes: 2 additions & 1 deletion src/Files.App/ServicesImplementation/DialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public DialogService()
{ typeof(ElevateConfirmDialogViewModel), () => new ElevateConfirmDialog() },
{ typeof(FileSystemDialogViewModel), () => new FilesystemOperationDialog() },
{ typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() },
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() }
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() },
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() }
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2866,6 +2866,18 @@
<data name="SortBy" xml:space="preserve">
<value>Sort by</value>
</data>
<data name="NewShortcutDialogTitle" xml:space="preserve">
<value>Create a new shortcut</value>
</data>
<data name="NewShortcutDialogDescription" xml:space="preserve">
<value>Create shortcuts to local or network programs, files, folders, computers or Internet addresses.</value>
</data>
<data name="NewShortcutDialogPrompt" xml:space="preserve">
<value>Enter the location of the item:</value>
</data>
<data name="AddDialogListShortcutSubHeader" xml:space="preserve">
<value>Creates a shortcut</value>
</data>
<data name="RecentFilesDisabledOnWindowsWarning" xml:space="preserve">
<value>Recently used files is currently disabled in Windows File Explorer.</value>
</data>
Expand Down
9 changes: 9 additions & 0 deletions src/Files.App/UserControls/InnerNavigationToolbar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@
<FontIcon Glyph="&#xE7C3;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Name="NewShortcut"
AutomationProperties.AutomationId="InnerNavigationToolbarNewShortcutButton"
Command="{x:Bind ViewModel.CreateNewShortcutCommand, Mode=OneWay}"
Text="{helpers:ResourceString Name=Shortcut}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE71B;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator x:Name="NewMenuFileFolderSeparator" />
</MenuFlyout>
</AppBarButton.Flyout>
Expand Down
100 changes: 100 additions & 0 deletions src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Files.App.Helpers;
using Files.Backend.Extensions;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.Storage.Pickers;

namespace Files.App.ViewModels.Dialogs
{
public class CreateShortcutDialogViewModel : ObservableObject
{
// User's working directory
public readonly string WorkingDirectory;

// Tells whether destination path exists
public bool DestinationPathExists { get; set; } = false;

// Destination of the shortcut chosen by the user (can be a path or a URL)
private string _destinationItemPath;
public string DestinationItemPath
{
get => _destinationItemPath;
set => SetProperty(ref _destinationItemPath, value);
}

// Tells if the selected destination is valid (Path exists or URL is well-formed). Used to enable primary button
private bool _isLocationValid;
public bool IsLocationValid
{
get => _isLocationValid;
set => SetProperty(ref _isLocationValid, value);
}

// Command invoked when the user clicks the 'Browse' button
public ICommand SelectDestinationCommand { get; private set; }

// Command invoked when the user clicks primary button
public ICommand PrimaryButtonCommand { get; private set; }

public CreateShortcutDialogViewModel(string workingDirectory)
{
WorkingDirectory = workingDirectory;
_destinationItemPath = string.Empty;

SelectDestinationCommand = new AsyncRelayCommand(SelectDestination);
PrimaryButtonCommand = new AsyncRelayCommand(CreateShortcut);
}

private async Task SelectDestination()
{
var folderPicker = InitializeWithWindow(new FolderPicker());
folderPicker.FileTypeFilter.Add("*");

var selectedFolder = await folderPicker.PickSingleFolderAsync();
if (selectedFolder is not null)
DestinationItemPath = selectedFolder.Path;
}

private FolderPicker InitializeWithWindow(FolderPicker obj)
{
WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle);
return obj;
}

private async Task CreateShortcut()
{
string? destinationName;
var extension = DestinationPathExists ? ".lnk" : ".url";

if (DestinationPathExists)
{
destinationName = Path.GetFileName(DestinationItemPath);
destinationName ??= Path.GetDirectoryName(DestinationItemPath);
}
else
{
var uri = new Uri(DestinationItemPath);
destinationName = uri.Host;
}

var shortcutName = string.Format("ShortcutCreateNewSuffix".ToLocalized(), destinationName);
var filePath = Path.Combine(
WorkingDirectory,
shortcutName + extension);

int fileNumber = 1;
while (Path.Exists(filePath))
{
filePath = Path.Combine(
WorkingDirectory,
shortcutName + $" ({++fileNumber})" + extension);
}

await FileOperationsHelpers.CreateOrUpdateLinkAsync(filePath, DestinationItemPath);
}
}
}
2 changes: 2 additions & 0 deletions src/Files.App/ViewModels/ToolbarViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,8 @@ public void SearchRegion_LostFocus(object sender, RoutedEventArgs e)

public ICommand? CreateNewFolderCommand { get; set; }

public ICommand? CreateNewShortcutCommand { get; set; }

public ICommand? CopyCommand { get; set; }

public ICommand? DeleteCommand { get; set; }
Expand Down
10 changes: 7 additions & 3 deletions src/Files.App/Views/ColumnShellPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ private void InitToolbarCommands()
ToolbarViewModel.ClosePaneCommand = new RelayCommand(() => PaneHolder?.CloseActivePane());
ToolbarViewModel.CreateNewFileCommand = new RelayCommand<ShellNewEntry>(x => UIFilesystemHelpers.CreateFileFromDialogResultType(AddItemDialogItemType.File, x, this));
ToolbarViewModel.CreateNewFolderCommand = new RelayCommand(() => UIFilesystemHelpers.CreateFileFromDialogResultType(AddItemDialogItemType.Folder, null, this));
ToolbarViewModel.CreateNewShortcutCommand = new RelayCommand(() => CreateNewShortcutFromDialog());
ToolbarViewModel.CopyCommand = new RelayCommand(async () => await UIFilesystemHelpers.CopyItem(this));
ToolbarViewModel.Rename = new RelayCommand(() => SlimContentPage?.CommandsViewModel.RenameItemCommand.Execute(null));
ToolbarViewModel.Share = new RelayCommand(() => SlimContentPage?.CommandsViewModel.ShareItemCommand.Execute(null));
Expand Down Expand Up @@ -689,13 +690,13 @@ private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, Keybo
{
var addItemDialogViewModel = new AddItemDialogViewModel();
await DialogService.ShowDialogAsync(addItemDialogViewModel);
if (addItemDialogViewModel.ResultType.ItemType != AddItemDialogItemType.Cancel)
{
if (addItemDialogViewModel.ResultType.ItemType == AddItemDialogItemType.Shortcut)
CreateNewShortcutFromDialog();
else if (addItemDialogViewModel.ResultType.ItemType != AddItemDialogItemType.Cancel)
UIFilesystemHelpers.CreateFileFromDialogResultType(
addItemDialogViewModel.ResultType.ItemType,
addItemDialogViewModel.ResultType.ItemInfo,
this);
}
}
break;

Expand Down Expand Up @@ -1045,5 +1046,8 @@ public void SubmitSearch(string query, bool searchUnindexedItems)
});
//this.FindAscendant<ColumnViewBrowser>().SetSelectedPathOrNavigate(null, typeof(ColumnViewBase), navArgs);
}

private async void CreateNewShortcutFromDialog()
=> await UIFilesystemHelpers.CreateShortcutFromDialogAsync(this);
}
}
Loading