Skip to content

Commit 1074a93

Browse files
authored
Fix: Fixed an issue where shortcuts couldn't be created for commands (#15934)
1 parent cd813bc commit 1074a93

File tree

5 files changed

+181
-26
lines changed

5 files changed

+181
-26
lines changed

src/Files.App/Dialogs/CreateShortcutDialog.xaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@
4646

4747
<!-- Path Box -->
4848
<TextBox
49-
x:Name="DestinationItemPath"
49+
x:Name="ShortcutTarget"
5050
Grid.Row="2"
5151
Grid.Column="0"
5252
HorizontalAlignment="Stretch"
5353
PlaceholderText="{x:Bind ViewModel.DestinationPlaceholder, Mode=OneWay}"
54-
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
54+
Text="{x:Bind ViewModel.ShortcutTarget, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
5555
<TextBox.Resources>
5656
<TeachingTip
5757
x:Name="InvalidPathWarning"

src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public CreateShortcutDialog()
2727

2828
InvalidPathWarning.SetBinding(TeachingTip.TargetProperty, new Binding()
2929
{
30-
Source = DestinationItemPath
30+
Source = ShortcutTarget
3131
});
3232
}
3333

src/Files.App/Helpers/UI/UIFilesystemHelpers.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,12 @@ public static async Task CreateShortcutFromDialogAsync(IShellPage associatedInst
198198
if (result != DialogResult.Primary || viewModel.ShortcutCreatedSuccessfully)
199199
return;
200200

201-
await HandleShortcutCannotBeCreated(viewModel.ShortcutCompleteName, viewModel.DestinationItemPath);
201+
await HandleShortcutCannotBeCreated(viewModel.ShortcutCompleteName, viewModel.FullPath, viewModel.Arguments);
202202

203203
await associatedInstance.RefreshIfNoWatcherExistsAsync();
204204
}
205205

206-
public static async Task<bool> HandleShortcutCannotBeCreated(string shortcutName, string destinationPath)
206+
public static async Task<bool> HandleShortcutCannotBeCreated(string shortcutName, string destinationPath, string arguments = "")
207207
{
208208
var result = await DialogDisplayHelper.ShowDialogAsync
209209
(
@@ -217,7 +217,7 @@ public static async Task<bool> HandleShortcutCannotBeCreated(string shortcutName
217217

218218
var shortcutPath = Path.Combine(Constants.UserEnvironmentPaths.DesktopPath, shortcutName);
219219

220-
return await FileOperationsHelpers.CreateOrUpdateLinkAsync(shortcutPath, destinationPath);
220+
return await FileOperationsHelpers.CreateOrUpdateLinkAsync(shortcutPath, destinationPath, arguments);
221221
}
222222

223223
/// <summary>

src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text;
77
using System.Windows.Input;
88
using Windows.Storage.Pickers;
9+
using Files.Shared.Helpers;
910

1011
namespace Files.App.ViewModels.Dialogs
1112
{
@@ -20,45 +21,151 @@ public sealed class CreateShortcutDialogViewModel : ObservableObject
2021
// Tells whether destination path exists
2122
public bool DestinationPathExists { get; set; }
2223

23-
// Tells wheteher the shortcut has been created
24+
// Tells whether the shortcut has been created
2425
public bool ShortcutCreatedSuccessfully { get; private set; }
2526

2627
// Shortcut name with extension
2728
public string ShortcutCompleteName { get; private set; } = string.Empty;
2829

29-
// Destination of the shortcut chosen by the user (can be a path or a URL)
30-
private string _destinationItemPath;
31-
public string DestinationItemPath
30+
// Full path of the destination item
31+
public string FullPath { get; private set; }
32+
33+
// Arguments to be passed to the destination item if it's an executable
34+
public string Arguments { get; private set; }
35+
36+
// Previous path of the destination item
37+
private string _previousShortcutTargetPath;
38+
39+
// Destination of the shortcut chosen by the user (can be a path, a command or a URL)
40+
private string _shortcutTarget;
41+
public string ShortcutTarget
3242
{
33-
get => _destinationItemPath;
43+
get => _shortcutTarget;
3444
set
3545
{
36-
if (!SetProperty(ref _destinationItemPath, value))
46+
if (!SetProperty(ref _shortcutTarget, value))
3747
return;
3848

3949
OnPropertyChanged(nameof(ShowWarningTip));
40-
if (string.IsNullOrWhiteSpace(DestinationItemPath))
50+
if (string.IsNullOrWhiteSpace(ShortcutTarget))
4151
{
52+
DestinationPathExists = false;
4253
IsLocationValid = false;
54+
_previousShortcutTargetPath = string.Empty;
4355
return;
4456
}
45-
4657
try
4758
{
48-
DestinationPathExists = Path.Exists(DestinationItemPath) && DestinationItemPath != Path.GetPathRoot(DestinationItemPath);
49-
if (DestinationPathExists)
59+
var trimmed = ShortcutTarget.Trim();
60+
// If the text starts with '"', try to parse the quoted part as path, and the rest as arguments
61+
if (trimmed.StartsWith('"'))
5062
{
51-
IsLocationValid = true;
63+
var endQuoteIndex = trimmed.IndexOf('"', 1);
64+
if (endQuoteIndex == -1)
65+
{
66+
DestinationPathExists = false;
67+
IsLocationValid = false;
68+
_previousShortcutTargetPath = string.Empty;
69+
return;
70+
}
71+
72+
var quoted = trimmed[1..endQuoteIndex];
73+
74+
if (quoted == _previousShortcutTargetPath)
75+
{
76+
Arguments = !Directory.Exists(FullPath) ? trimmed[(endQuoteIndex + 1)..] : string.Empty;
77+
return;
78+
}
79+
80+
if (IsValidAbsolutePath(quoted))
81+
{
82+
DestinationPathExists = true;
83+
IsLocationValid = true;
84+
FullPath = Path.GetFullPath(quoted);
85+
Arguments = !Directory.Exists(FullPath) ? trimmed[(endQuoteIndex + 1)..] : string.Empty;
86+
_previousShortcutTargetPath = quoted;
87+
return;
88+
}
89+
90+
// If the quoted part is a valid filename, try to find it in the PATH
91+
if (quoted == Path.GetFileName(quoted)
92+
&& quoted.IndexOfAny(Path.GetInvalidFileNameChars()) == -1
93+
&& PathHelpers.TryGetFullPath(quoted, out var fullPath))
94+
{
95+
DestinationPathExists = true;
96+
IsLocationValid = true;
97+
FullPath = fullPath;
98+
Arguments = trimmed[(endQuoteIndex + 1)..];
99+
_previousShortcutTargetPath = quoted;
100+
return;
101+
}
102+
103+
var uri = new Uri(quoted);
104+
DestinationPathExists = false;
105+
IsLocationValid = uri.IsWellFormedOriginalString();
106+
FullPath = quoted;
107+
Arguments = string.Empty;
108+
_previousShortcutTargetPath = string.Empty;
52109
}
53110
else
54111
{
55-
var uri = new Uri(DestinationItemPath);
112+
var filePath = trimmed.Split(' ')[0];
113+
114+
if (filePath == _previousShortcutTargetPath)
115+
{
116+
Arguments = !Directory.Exists(FullPath) ? trimmed.Split(' ')[1..].Aggregate(string.Empty, (current, arg) => current + arg + " ") : string.Empty;
117+
return;
118+
}
119+
120+
if (IsValidAbsolutePath(filePath))
121+
{
122+
DestinationPathExists = true;
123+
IsLocationValid = true;
124+
FullPath = Path.GetFullPath(filePath);
125+
Arguments = !Directory.Exists(FullPath) ? trimmed.Split(' ')[1..].Aggregate(string.Empty, (current, arg) => current + arg + " ") : string.Empty;
126+
_previousShortcutTargetPath = filePath;
127+
return;
128+
}
129+
130+
// Try to parse the whole text as path
131+
if (IsValidAbsolutePath(trimmed))
132+
{
133+
DestinationPathExists = true;
134+
IsLocationValid = true;
135+
FullPath = Path.GetFullPath(trimmed);
136+
Arguments = string.Empty;
137+
_previousShortcutTargetPath = string.Empty;
138+
return;
139+
}
140+
141+
if (filePath == Path.GetFileName(filePath)
142+
&& filePath.IndexOfAny(Path.GetInvalidFileNameChars()) == -1
143+
&& PathHelpers.TryGetFullPath(filePath, out var fullPath))
144+
{
145+
DestinationPathExists = true;
146+
IsLocationValid = true;
147+
FullPath = fullPath;
148+
Arguments = trimmed.Split(' ')[1..].Aggregate(string.Empty, (current, arg) => current + arg + " ");
149+
_previousShortcutTargetPath = filePath;
150+
return;
151+
}
152+
153+
var uri = new Uri(trimmed);
154+
DestinationPathExists = false;
56155
IsLocationValid = uri.IsWellFormedOriginalString();
156+
FullPath = trimmed;
157+
Arguments = string.Empty;
158+
_previousShortcutTargetPath = string.Empty;
57159
}
160+
58161
}
59162
catch (Exception)
60163
{
164+
DestinationPathExists = false;
61165
IsLocationValid = false;
166+
FullPath = string.Empty;
167+
Arguments = string.Empty;
168+
_previousShortcutTargetPath = string.Empty;
62169
}
63170
}
64171
}
@@ -75,7 +182,7 @@ public bool IsLocationValid
75182
}
76183
}
77184

78-
public bool ShowWarningTip => !string.IsNullOrEmpty(DestinationItemPath) && !_isLocationValid;
185+
public bool ShowWarningTip => !string.IsNullOrEmpty(ShortcutTarget) && !_isLocationValid;
79186

80187
// Command invoked when the user clicks the 'Browse' button
81188
public ICommand SelectDestinationCommand { get; private set; }
@@ -86,12 +193,17 @@ public bool IsLocationValid
86193
public CreateShortcutDialogViewModel(string workingDirectory)
87194
{
88195
WorkingDirectory = workingDirectory;
89-
_destinationItemPath = string.Empty;
196+
_shortcutTarget = string.Empty;
90197

91198
SelectDestinationCommand = new AsyncRelayCommand(SelectDestination);
92199
PrimaryButtonCommand = new AsyncRelayCommand(CreateShortcutAsync);
93200
}
94201

202+
private bool IsValidAbsolutePath(string path)
203+
{
204+
return Path.Exists(path) && Path.IsPathFullyQualified(path) && path != Path.GetPathRoot(path);
205+
}
206+
95207
private Task SelectDestination()
96208
{
97209
Win32PInvoke.BROWSEINFO bi = new Win32PInvoke.BROWSEINFO();
@@ -103,7 +215,7 @@ private Task SelectDestination()
103215
StringBuilder path = new StringBuilder(260);
104216
if (Win32PInvoke.SHGetPathFromIDList(pidl, path))
105217
{
106-
DestinationItemPath = path.ToString();
218+
ShortcutTarget = path.ToString();
107219
}
108220
Marshal.FreeCoTaskMem(pidl);
109221
}
@@ -118,10 +230,12 @@ private async Task CreateShortcutAsync()
118230

119231
if (DestinationPathExists)
120232
{
121-
destinationName = Path.GetFileName(DestinationItemPath);
122-
if (string.IsNullOrEmpty(destinationName))
233+
destinationName = Path.GetFileName(FullPath);
234+
235+
if(string.IsNullOrEmpty(FullPath))
123236
{
124-
var destinationPath = DestinationItemPath.Replace('/', '\\');
237+
238+
var destinationPath = FullPath.Replace('/', '\\');
125239

126240
if (destinationPath.EndsWith('\\'))
127241
destinationPath = destinationPath.Substring(0, destinationPath.Length - 1);
@@ -131,7 +245,7 @@ private async Task CreateShortcutAsync()
131245
}
132246
else
133247
{
134-
var uri = new Uri(DestinationItemPath);
248+
var uri = new Uri(FullPath);
135249
destinationName = uri.Host;
136250
}
137251

@@ -146,7 +260,7 @@ private async Task CreateShortcutAsync()
146260
filePath = Path.Combine(WorkingDirectory, ShortcutCompleteName);
147261
}
148262

149-
ShortcutCreatedSuccessfully = await FileOperationsHelpers.CreateOrUpdateLinkAsync(filePath, DestinationItemPath);
263+
ShortcutCreatedSuccessfully = await FileOperationsHelpers.CreateOrUpdateLinkAsync(filePath, FullPath, Arguments);
150264
}
151265
}
152266
}

src/Files.Shared/Helpers/PathHelpers.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO;
67

78
namespace Files.Shared.Helpers
@@ -63,5 +64,45 @@ public static bool IsSpecialFolder(string path)
6364

6465
return false;
6566
}
67+
68+
public static bool TryGetFullPath(string commandName, out string fullPath)
69+
{
70+
fullPath = string.Empty;
71+
try
72+
{
73+
var p = new Process();
74+
p.StartInfo = new ProcessStartInfo
75+
{
76+
UseShellExecute = false,
77+
CreateNoWindow = true,
78+
FileName = "where.exe",
79+
Arguments = commandName,
80+
RedirectStandardOutput = true
81+
};
82+
p.Start();
83+
var output = p.StandardOutput.ReadToEnd();
84+
p.WaitForExit(1000);
85+
86+
87+
if (p.ExitCode != 0)
88+
return false;
89+
90+
// Return the first one with valid executable extension, in case there is a match with no extension
91+
foreach (var line in output.Split(Environment.NewLine))
92+
{
93+
if (FileExtensionHelpers.IsExecutableFile(line))
94+
{
95+
fullPath = line;
96+
return true;
97+
}
98+
}
99+
return false;
100+
}
101+
catch (Exception)
102+
{
103+
return false;
104+
}
105+
106+
}
66107
}
67108
}

0 commit comments

Comments
 (0)