diff --git a/src/Files.App/Actions/Open/OpenInIDEAction.cs b/src/Files.App/Actions/Open/OpenInIDEAction.cs new file mode 100644 index 000000000000..4c2d118ce42c --- /dev/null +++ b/src/Files.App/Actions/Open/OpenInIDEAction.cs @@ -0,0 +1,64 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Actions +{ + internal sealed partial class OpenInIDEAction : ObservableObject, IAction + { + private readonly IDevToolsSettingsService _devToolsSettingsService; + + private readonly IContentPageContext _context; + + public string Label + => string.Format( + "OpenInIDE".GetLocalizedResource(), + _devToolsSettingsService.IDEName); + + public string Description + => string.Format( + "OpenInIDEDescription".GetLocalizedResource(), + _devToolsSettingsService.IDEName); + + public bool IsExecutable => + _context.Folder is not null && + !string.IsNullOrWhiteSpace(_devToolsSettingsService.IDEPath); + + public OpenInIDEAction() + { + _devToolsSettingsService = Ioc.Default.GetRequiredService(); + _context = Ioc.Default.GetRequiredService(); + _context.PropertyChanged += Context_PropertyChanged; + _devToolsSettingsService.PropertyChanged += DevSettings_PropertyChanged; + } + + public async Task ExecuteAsync(object? parameter = null) + { + var res = await Win32Helper.RunPowershellCommandAsync( + $"& \'{_devToolsSettingsService.IDEPath}\' \'{_context.ShellPage?.ShellViewModel.WorkingDirectory}\'", + PowerShellExecutionOptions.Hidden + ); + + if (!res) + await DynamicDialogFactory.ShowFor_IDEErrorDialog(_devToolsSettingsService.IDEName); + } + + private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IContentPageContext.Folder)) + OnPropertyChanged(nameof(IsExecutable)); + } + + private void DevSettings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IDevToolsSettingsService.IDEPath)) + { + OnPropertyChanged(nameof(IsExecutable)); + } + else if (e.PropertyName == nameof(IDevToolsSettingsService.IDEName)) + { + OnPropertyChanged(nameof(Label)); + OnPropertyChanged(nameof(Description)); + } + } + } +} diff --git a/src/Files.App/Actions/Open/OpenInVSCodeAction.cs b/src/Files.App/Actions/Open/OpenInVSCodeAction.cs deleted file mode 100644 index ffb26efd988d..000000000000 --- a/src/Files.App/Actions/Open/OpenInVSCodeAction.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using Files.App.Utils.Shell; - -namespace Files.App.Actions -{ - internal sealed partial class OpenInVSCodeAction : ObservableObject, IAction - { - private readonly IContentPageContext _context; - - private readonly bool _isVSCodeInstalled; - - public string Label - => "OpenInVSCode".GetLocalizedResource(); - - public string Description - => "OpenInVSCodeDescription".GetLocalizedResource(); - - public bool IsExecutable => - _isVSCodeInstalled && - _context.Folder is not null; - - public OpenInVSCodeAction() - { - _context = Ioc.Default.GetRequiredService(); - - _isVSCodeInstalled = SoftwareHelpers.IsVSCodeInstalled(); - if (_isVSCodeInstalled) - _context.PropertyChanged += Context_PropertyChanged; - } - - public Task ExecuteAsync(object? parameter = null) - { - return Win32Helper.RunPowershellCommandAsync($"code \'{_context.ShellPage?.ShellViewModel.WorkingDirectory}\'", PowerShellExecutionOptions.Hidden); - } - - private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(IContentPageContext.Folder)) - OnPropertyChanged(nameof(IsExecutable)); - } - } -} diff --git a/src/Files.App/Actions/Open/OpenRepoInIDEAction.cs b/src/Files.App/Actions/Open/OpenRepoInIDEAction.cs new file mode 100644 index 000000000000..d115ac8ec013 --- /dev/null +++ b/src/Files.App/Actions/Open/OpenRepoInIDEAction.cs @@ -0,0 +1,61 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Actions +{ + internal sealed partial class OpenRepoInIDEAction : ObservableObject, IAction + { + private readonly IDevToolsSettingsService _devToolsSettingsService; + + private readonly IContentPageContext _context; + + public string Label + => string.Format("OpenRepoInIDE".GetLocalizedResource(), _devToolsSettingsService.IDEName); + + public string Description + => string.Format("OpenRepoInIDEDescription".GetLocalizedResource(), _devToolsSettingsService.IDEName); + + public bool IsExecutable => + _context.Folder is not null && + _context.ShellPage!.InstanceViewModel.IsGitRepository && + !string.IsNullOrWhiteSpace(_devToolsSettingsService.IDEPath); + + public OpenRepoInIDEAction() + { + _context = Ioc.Default.GetRequiredService(); + _devToolsSettingsService = Ioc.Default.GetRequiredService(); + _context.PropertyChanged += Context_PropertyChanged; + _devToolsSettingsService.PropertyChanged += DevSettings_PropertyChanged; + } + + public async Task ExecuteAsync(object? parameter = null) + { + var res = await Win32Helper.RunPowershellCommandAsync( + $"& \'{_devToolsSettingsService.IDEPath}\' \'{_context.ShellPage!.InstanceViewModel.GitRepositoryPath}\'", + PowerShellExecutionOptions.Hidden + ); + + if (!res) + await DynamicDialogFactory.ShowFor_IDEErrorDialog(_devToolsSettingsService.IDEName); + } + + private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IContentPageContext.Folder)) + OnPropertyChanged(nameof(IsExecutable)); + } + + private void DevSettings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IDevToolsSettingsService.IDEPath)) + { + OnPropertyChanged(nameof(IsExecutable)); + } + else if (e.PropertyName == nameof(IDevToolsSettingsService.IDEName)) + { + OnPropertyChanged(nameof(Label)); + OnPropertyChanged(nameof(Description)); + } + } + } +} diff --git a/src/Files.App/Actions/Open/OpenRepoInVSCodeAction.cs b/src/Files.App/Actions/Open/OpenRepoInVSCodeAction.cs deleted file mode 100644 index abe1e14a5399..000000000000 --- a/src/Files.App/Actions/Open/OpenRepoInVSCodeAction.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using Files.App.Utils.Shell; - -namespace Files.App.Actions -{ - internal sealed partial class OpenRepoInVSCodeAction : ObservableObject, IAction - { - private readonly IContentPageContext _context; - - private readonly bool _isVSCodeInstalled; - - public string Label - => "OpenRepoInVSCode".GetLocalizedResource(); - - public string Description - => "OpenRepoInVSCodeDescription".GetLocalizedResource(); - - public bool IsExecutable => - _isVSCodeInstalled && - _context.Folder is not null && - _context.ShellPage!.InstanceViewModel.IsGitRepository; - - public OpenRepoInVSCodeAction() - { - _context = Ioc.Default.GetRequiredService(); - - _isVSCodeInstalled = SoftwareHelpers.IsVSCodeInstalled(); - if (_isVSCodeInstalled) - _context.PropertyChanged += Context_PropertyChanged; - } - - public Task ExecuteAsync(object? parameter = null) - { - return Win32Helper.RunPowershellCommandAsync($"code \'{_context.ShellPage!.InstanceViewModel.GitRepositoryPath}\'", PowerShellExecutionOptions.Hidden); - } - - private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(IContentPageContext.Folder)) - OnPropertyChanged(nameof(IsExecutable)); - } - } -} diff --git a/src/Files.App/Data/Commands/Manager/CommandCodes.cs b/src/Files.App/Data/Commands/Manager/CommandCodes.cs index 19c2158a0a29..b00898587aaf 100644 --- a/src/Files.App/Data/Commands/Manager/CommandCodes.cs +++ b/src/Files.App/Data/Commands/Manager/CommandCodes.cs @@ -112,8 +112,8 @@ public enum CommandCodes RotateRight, // Open - OpenInVSCode, - OpenRepoInVSCode, + OpenInIDE, + OpenRepoInIDE, OpenProperties, OpenReleaseNotes, OpenClassicProperties, diff --git a/src/Files.App/Data/Commands/Manager/CommandManager.cs b/src/Files.App/Data/Commands/Manager/CommandManager.cs index f0e656b11943..6dc09ae4fa3f 100644 --- a/src/Files.App/Data/Commands/Manager/CommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/CommandManager.cs @@ -113,8 +113,8 @@ public IRichCommand this[HotKey hotKey] public IRichCommand OpenItem => commands[CommandCodes.OpenItem]; public IRichCommand OpenItemWithApplicationPicker => commands[CommandCodes.OpenItemWithApplicationPicker]; public IRichCommand OpenParentFolder => commands[CommandCodes.OpenParentFolder]; - public IRichCommand OpenInVSCode => commands[CommandCodes.OpenInVSCode]; - public IRichCommand OpenRepoInVSCode => commands[CommandCodes.OpenRepoInVSCode]; + public IRichCommand OpenInVSCode => commands[CommandCodes.OpenInIDE]; + public IRichCommand OpenRepoInVSCode => commands[CommandCodes.OpenRepoInIDE]; public IRichCommand OpenProperties => commands[CommandCodes.OpenProperties]; public IRichCommand OpenReleaseNotes => commands[CommandCodes.OpenReleaseNotes]; public IRichCommand OpenClassicProperties => commands[CommandCodes.OpenClassicProperties]; @@ -317,8 +317,8 @@ public IEnumerator GetEnumerator() => [CommandCodes.OpenItem] = new OpenItemAction(), [CommandCodes.OpenItemWithApplicationPicker] = new OpenItemWithApplicationPickerAction(), [CommandCodes.OpenParentFolder] = new OpenParentFolderAction(), - [CommandCodes.OpenInVSCode] = new OpenInVSCodeAction(), - [CommandCodes.OpenRepoInVSCode] = new OpenRepoInVSCodeAction(), + [CommandCodes.OpenInIDE] = new OpenInIDEAction(), + [CommandCodes.OpenRepoInIDE] = new OpenRepoInIDEAction(), [CommandCodes.OpenProperties] = new OpenPropertiesAction(), [CommandCodes.OpenReleaseNotes] = new OpenReleaseNotesAction(), [CommandCodes.OpenClassicProperties] = new OpenClassicPropertiesAction(), diff --git a/src/Files.App/Data/Contracts/IDevToolsSettingsService.cs b/src/Files.App/Data/Contracts/IDevToolsSettingsService.cs index 113ff6732888..2148c8322722 100644 --- a/src/Files.App/Data/Contracts/IDevToolsSettingsService.cs +++ b/src/Files.App/Data/Contracts/IDevToolsSettingsService.cs @@ -1,9 +1,6 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Media; - namespace Files.App.Data.Contracts { public interface IDevToolsSettingsService : IBaseSettingsService, INotifyPropertyChanged @@ -12,5 +9,15 @@ public interface IDevToolsSettingsService : IBaseSettingsService, INotifyPropert /// Gets or sets a value when the Open in IDE button should be displayed on the status bar. /// OpenInIDEOption OpenInIDEOption { get; set; } + + /// + /// Gets or sets the path of the chosen IDE. + /// + string IDEPath { get; set; } + + /// + /// Gets or sets the name of the chosen IDE. + /// + string IDEName { get; set; } } } diff --git a/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs b/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs index a04f8c24f380..2f4f333df65b 100644 --- a/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs +++ b/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs @@ -404,5 +404,25 @@ public static DynamicDialog GetFor_CreateAlternateDataStreamDialog() return dialog; } + + public static async Task ShowFor_IDEErrorDialog(string friendlyName) + { + var commands = Ioc.Default.GetRequiredService(); + var dialog = new DynamicDialog(new DynamicDialogViewModel() + { + TitleText = Strings.IDENotLocatedTitle.GetLocalizedResource(), + SubtitleText = string.Format(Strings.IDENotLocatedContent.GetLocalizedResource(), friendlyName), + PrimaryButtonText = Strings.OpenSettings.GetLocalizedResource(), + SecondaryButtonText = Strings.Close.GetLocalizedResource(), + DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Secondary, + }); + + await dialog.TryShowAsync(); + + if (dialog.DynamicResult is DynamicDialogResult.Primary) + await commands.OpenSettings.ExecuteAsync( + new SettingsNavigationParams() { PageKind = SettingsPageKind.DevToolsPage } + ); + } } } diff --git a/src/Files.App/Helpers/Environment/SoftwareHelpers.cs b/src/Files.App/Helpers/Environment/SoftwareHelpers.cs index 4b674564ba89..a87fedd14260 100644 --- a/src/Files.App/Helpers/Environment/SoftwareHelpers.cs +++ b/src/Files.App/Helpers/Environment/SoftwareHelpers.cs @@ -9,11 +9,9 @@ namespace Files.App.Helpers internal static class SoftwareHelpers { private const string UninstallRegistryKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; - private const string VsRegistryKey = @"SOFTWARE\Microsoft\VisualStudio"; private const string VsCodeName = "Microsoft Visual Studio Code"; - public static bool IsVSCodeInstalled() { try @@ -29,25 +27,6 @@ public static bool IsVSCodeInstalled() } } - public static bool IsVSInstalled() - { - try - { - var key = Registry.LocalMachine.OpenSubKey(VsRegistryKey); - if (key is null) - return false; - - key.Close(); - - return true; - } - catch (SecurityException) - { - // Handle edge case where OpenSubKey results in SecurityException - return false; - } - } - private static bool ContainsName(RegistryKey? key, string find) { if (key is null) diff --git a/src/Files.App/Services/Settings/DevToolsSettingsService.cs b/src/Files.App/Services/Settings/DevToolsSettingsService.cs index 7d2fb980f29d..883c4e4e489e 100644 --- a/src/Files.App/Services/Settings/DevToolsSettingsService.cs +++ b/src/Files.App/Services/Settings/DevToolsSettingsService.cs @@ -18,6 +18,20 @@ public OpenInIDEOption OpenInIDEOption set => Set(value); } + /// + public string IDEPath + { + get => Get(SoftwareHelpers.IsVSCodeInstalled() ? "code" : string.Empty) ?? string.Empty; + set => Set(value); + } + + /// + public string IDEName + { + get => Get(SoftwareHelpers.IsVSCodeInstalled() ? Strings.VisualStudioCode.GetLocalizedResource() : string.Empty) ?? string.Empty; + set => Set(value); + } + protected override void RaiseOnSettingChangedEvent(object sender, SettingChangedEventArgs e) { base.RaiseOnSettingChangedEvent(sender, e); diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 119345bacf36..82f90390b9fe 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3214,17 +3214,17 @@ Files cannot access GitHub right now. - - Open folder in VS Code + + Open folder in {0} - - Open the current directory in Visual Studio Code + + Open the current directory in {0} - - Open repo in VS Code + + Open repo in {0} - - Open the root of the Git repo in Visual Studio Code + + Open the root of the Git repo in {0} Copy code @@ -4043,4 +4043,31 @@ Add to shelf Tooltip that displays when dragging items to the Shelf Pane + + Path or alias + + + Invalid path + + + Test integration + + + {0} could not be located. Please check your settings and try again. + + + The configured IDE could not be located + + + Open settings + + + Visual Studio Code + + + Enter a path or launch alias + + + Please enter a name for the IDE + \ No newline at end of file diff --git a/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs b/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs index cc80c13b490d..b284fd9b1238 100644 --- a/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs +++ b/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs @@ -1,6 +1,7 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.IO; using System.Windows.Input; namespace Files.App.ViewModels.Settings @@ -9,10 +10,16 @@ public sealed partial class DevToolsViewModel : ObservableObject { protected readonly IFileTagsSettingsService FileTagsSettingsService = Ioc.Default.GetRequiredService(); protected readonly IDevToolsSettingsService DevToolsSettingsService = Ioc.Default.GetRequiredService(); + private readonly ICommonDialogService CommonDialogService = Ioc.Default.GetRequiredService(); public Dictionary OpenInIDEOptions { get; private set; } = []; public ICommand RemoveCredentialsCommand { get; } public ICommand ConnectToGitHubCommand { get; } + public ICommand StartEditingIDECommand { get; } + public ICommand CancelIDEChangesCommand { get; } + public ICommand SaveIDEChangesCommand { get; } + public ICommand OpenFilePickerForIDECommand { get; } + public ICommand TestIDECommand { get; } // Enabled when there are saved credentials private bool _IsLogoutEnabled; @@ -22,6 +29,63 @@ public bool IsLogoutEnabled set => SetProperty(ref _IsLogoutEnabled, value); } + private bool _IsEditingIDEConfig; + public bool IsEditingIDEConfig + { + get => _IsEditingIDEConfig; + set => SetProperty(ref _IsEditingIDEConfig, value); + } + + public bool CanSaveIDEChanges => + IsIDENameValid && IsIDEPathValid; + + private bool _IsIDEPathValid; + public bool IsIDEPathValid + { + get => _IsIDEPathValid; + set => SetProperty(ref _IsIDEPathValid, value); + } + + private bool _IsIDENameValid; + public bool IsIDENameValid + { + get => _IsIDENameValid; + set => SetProperty(ref _IsIDENameValid, value); + } + + private string _IDEPath; + public string IDEPath + { + get => _IDEPath; + set + { + if (SetProperty(ref _IDEPath, value)) + { + IsIDEPathValid = + !string.IsNullOrWhiteSpace(value) && + !value.Contains('\"') && + !value.Contains('\'') && + CheckPathExists(); + + OnPropertyChanged(nameof(CanSaveIDEChanges)); + } + } + } + + private string _IDEName; + public string IDEName + { + get => _IDEName; + set + { + if (SetProperty(ref _IDEName, value)) + { + IsIDENameValid = !string.IsNullOrEmpty(value); + OnPropertyChanged(nameof(CanSaveIDEChanges)); + } + } + } + public DevToolsViewModel() { // Open in IDE options @@ -29,10 +93,20 @@ public DevToolsViewModel() OpenInIDEOptions.Add(OpenInIDEOption.AllLocations, "AllLocations".GetLocalizedResource()); SelectedOpenInIDEOption = OpenInIDEOptions[DevToolsSettingsService.OpenInIDEOption]; + IDEPath = DevToolsSettingsService.IDEPath; + IDEName = DevToolsSettingsService.IDEName; + IsIDEPathValid = true; + IsIDENameValid = true; + IsLogoutEnabled = GitHelpers.GetSavedCredentials() != string.Empty; RemoveCredentialsCommand = new RelayCommand(DoRemoveCredentials); ConnectToGitHubCommand = new RelayCommand(DoConnectToGitHubAsync); + CancelIDEChangesCommand = new RelayCommand(DoCancelIDEChanges); + SaveIDEChangesCommand = new RelayCommand(DoSaveIDEChanges); + StartEditingIDECommand = new RelayCommand(DoStartEditingIDE); + OpenFilePickerForIDECommand = new RelayCommand(DoOpenFilePickerForIDE); + TestIDECommand = new RelayCommand(DoTestIDE); } private string selectedOpenInIDEOption; @@ -62,5 +136,65 @@ public async void DoConnectToGitHubAsync() await GitHelpers.RequireGitAuthenticationAsync(); } + + private void DoCancelIDEChanges() + { + IsEditingIDEConfig = false; + IDEPath = DevToolsSettingsService.IDEPath; + IDEName = DevToolsSettingsService.IDEName; + IsIDEPathValid = true; + IsIDENameValid = true; + } + + private void DoSaveIDEChanges() + { + IsEditingIDEConfig = false; + IsIDEPathValid = true; + IsIDENameValid = true; + DevToolsSettingsService.IDEPath = IDEPath; + DevToolsSettingsService.IDEName = IDEName; + } + + private void DoStartEditingIDE() + { + IsEditingIDEConfig = true; + } + + private void DoOpenFilePickerForIDE() + { + var res = CommonDialogService.Open_FileOpenDialog( + MainWindow.Instance.WindowHandle, + false, + ["*.exe;*.bat;*.cmd;*.ahk"], + Environment.SpecialFolder.ProgramFiles, + out var filePath + ); + + if (res) + IDEPath = filePath; + } + + private async void DoTestIDE() + { + IsIDEPathValid = await Win32Helper.RunPowershellCommandAsync( + $"& \'{IDEPath}\'", + PowerShellExecutionOptions.Hidden + ); + } + + private bool CheckPathExists() + { + if (Path.Exists(IDEPath)) + return true; + + var paths = Environment.GetEnvironmentVariable("PATH")?.Split(';'); + foreach (var path in paths ?? Array.Empty()) + { + if (Path.Exists(Path.Combine(path, IDEPath))) + return true; + } + + return false; + } } } diff --git a/src/Files.App/Views/Settings/DevToolsPage.xaml b/src/Files.App/Views/Settings/DevToolsPage.xaml index 6a98a5ebb51e..5f7c7b610e7e 100644 --- a/src/Files.App/Views/Settings/DevToolsPage.xaml +++ b/src/Files.App/Views/Settings/DevToolsPage.xaml @@ -11,6 +11,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:uc="using:Files.App.UserControls" xmlns:vm="using:Files.App.ViewModels.Settings" + xmlns:wctconverters="using:CommunityToolkit.WinUI.Converters" mc:Ignorable="d"> @@ -20,6 +21,14 @@ + + @@ -49,6 +58,160 @@ AutomationProperties.Name="{helpers:ResourceString Name=DisplayOpenIDE}" ItemsSource="{x:Bind ViewModel.OpenInIDEOptions.Values}" SelectedItem="{x:Bind ViewModel.SelectedOpenInIDEOption, Mode=TwoWay}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +