From 30a35e17f580cef38c27d3a69e43496990d6d7c2 Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Tue, 9 Nov 2021 12:06:42 -0800 Subject: [PATCH 01/29] Split old rename UI into folder and add basic new rename UI --- ...t.CodeAnalysis.EditorFeatures.Cocoa.csproj | 2 +- .../UI/Adornment/InlineRenameAdornment.xaml | 66 +++++ .../Adornment/InlineRenameAdornment.xaml.cs | 132 ++++++++++ .../InlineRenameAdornmentViewModel.cs | 240 ++++++++++++++++++ .../{ => UI}/Dashboard/Dashboard.xaml | 12 +- .../{ => UI}/Dashboard/Dashboard.xaml.cs | 0 .../Dashboard/DashboardAutomationPeer.cs | 0 .../{ => UI}/Dashboard/DashboardSeverity.cs | 0 .../{ => UI}/Dashboard/DashboardViewModel.cs | 0 .../{ => UI}/Dashboard/Images/ErrorIcon.png | Bin .../{ => UI}/Dashboard/Images/InfoIcon.png | Bin .../{ => UI}/Dashboard/RenameShortcutKeys.cs | 0 .../IInlineRenameColorUpdater.cs} | 4 +- .../InlineRenameAdornmentManager.cs} | 32 ++- .../InlineRenameAdornmentProvider.cs} | 14 +- .../InlineRenameColors.cs} | 2 +- .../InlineRenameColors.xaml} | 0 ...oft.CodeAnalysis.EditorFeatures.Wpf.csproj | 4 +- .../InlineRename/DashboardColorUpdater.cs | 16 +- 19 files changed, 490 insertions(+), 34 deletions(-) create mode 100644 src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml create mode 100644 src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs create mode 100644 src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/Dashboard.xaml (96%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/Dashboard.xaml.cs (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/DashboardAutomationPeer.cs (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/DashboardSeverity.cs (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/DashboardViewModel.cs (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/Images/ErrorIcon.png (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/Images/InfoIcon.png (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{ => UI}/Dashboard/RenameShortcutKeys.cs (100%) rename src/EditorFeatures/Core.Wpf/InlineRename/{Dashboard/IDashboardColorUpdater.cs => UI/IInlineRenameColorUpdater.cs} (84%) rename src/EditorFeatures/Core.Wpf/InlineRename/{Dashboard/DashboardAdornmentManager.cs => UI/InlineRenameAdornmentManager.cs} (68%) rename src/EditorFeatures/Core.Wpf/InlineRename/{Dashboard/DashboardAdornmentProvider.cs => UI/InlineRenameAdornmentProvider.cs} (80%) rename src/EditorFeatures/Core.Wpf/InlineRename/{Dashboard/DashboardColors.cs => UI/InlineRenameColors.cs} (96%) rename src/EditorFeatures/Core.Wpf/InlineRename/{Dashboard/DashboardColors.xaml => UI/InlineRenameColors.xaml} (100%) diff --git a/src/EditorFeatures/Core.Cocoa/Microsoft.CodeAnalysis.EditorFeatures.Cocoa.csproj b/src/EditorFeatures/Core.Cocoa/Microsoft.CodeAnalysis.EditorFeatures.Cocoa.csproj index ef02ec45167a3..43d7aa138874f 100644 --- a/src/EditorFeatures/Core.Cocoa/Microsoft.CodeAnalysis.EditorFeatures.Cocoa.csproj +++ b/src/EditorFeatures/Core.Cocoa/Microsoft.CodeAnalysis.EditorFeatures.Cocoa.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml new file mode 100644 index 0000000000000..683c285951c83 --- /dev/null +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs new file mode 100644 index 0000000000000..1f8351593fe92 --- /dev/null +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.CodeAnalysis.Editor.InlineRename.Adornment +{ + /// + /// Interaction logic for InlineRenameAdornment.xaml + /// + internal partial class InlineRenameAdornment : UserControl, IDisposable + { + private readonly InlineRenameAdornmentViewModel _viewModel; + private readonly ITextView _textView; + + public InlineRenameAdornment(InlineRenameAdornmentViewModel viewModel, ITextView textView) + { + DataContext = _viewModel = viewModel; + _textView = textView; + + _textView.LayoutChanged += TextView_LayoutChanged; + _textView.ViewportHeightChanged += TextView_ViewPortChanged; + _textView.ViewportWidthChanged += TextView_ViewPortChanged; + _textView.LostAggregateFocus += TextView_LostFocus; + _textView.Caret.PositionChanged += TextView_CursorChanged; + + // On initialization focus the first tab target + Initialized += (s, e) => MoveFocus(new TraversalRequest(FocusNavigationDirection.First)); + + InitializeComponent(); + PositionAdornment(); + } + +#pragma warning disable CA1822 // Mark members as static - used in xaml + public string RenameOverloads => EditorFeaturesResources.Include_overload_s; + public string SearchInComments => EditorFeaturesResources.Include_comments; + public string SearchInStrings => EditorFeaturesResources.Include_strings; + public string ApplyRename => EditorFeaturesResources.Apply1; + public string CancelRename => EditorFeaturesResources.Cancel; + public string PreviewChanges => EditorFeaturesResources.Preview_changes1; +#pragma warning restore CA1822 // Mark members as static + + private void TextView_CursorChanged(object sender, CaretPositionChangedEventArgs e) + => _viewModel.Cancel(); + + private void TextView_LostFocus(object sender, EventArgs e) + => _viewModel.Cancel(); + + private void TextView_ViewPortChanged(object sender, EventArgs e) + => PositionAdornment(); + + private void TextView_LayoutChanged(object sender, TextViewLayoutChangedEventArgs e) + => PositionAdornment(); + + private void PositionAdornment() + { + var top = _textView.Caret.Bottom + 5; + var left = _textView.Caret.Left - 5; + + Canvas.SetTop(this, top); + Canvas.SetLeft(this, left); + } + + public void Dispose() + { + _viewModel.Dispose(); + + _textView.LayoutChanged -= TextView_LayoutChanged; + _textView.ViewportHeightChanged -= TextView_ViewPortChanged; + _textView.ViewportWidthChanged -= TextView_ViewPortChanged; + _textView.LostAggregateFocus -= TextView_LostFocus; + _textView.Caret.PositionChanged -= TextView_CursorChanged; + } + + private void Submit_Click(object sender, RoutedEventArgs e) + { + _viewModel.Submit(); + } + + private void Adornment_KeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + e.Handled = true; + _viewModel.Submit(); + break; + + case Key.Escape: + e.Handled = true; + _viewModel.Cancel(); + break; + + case Key.Tab: + // We don't want tab to lose focus for the adornment, so manually + // loop focus back to the first item that is focusable. + FrameworkElement lastItem = OptionsExpander.IsExpanded + ? PreviewChangesCheckbox + : OptionsExpander; + + if (lastItem.IsFocused) + { + e.Handled = true; + MoveFocus(new TraversalRequest(FocusNavigationDirection.First)); + } + + break; + } + } + + private void IdentifierTextBox_GotFocus(object sender, RoutedEventArgs e) + { + IdentifierTextBox.SelectAll(); + } + + private void Adornment_ConsumeMouseEvent(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + } + + private void Adornment_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + MoveFocus(new TraversalRequest(FocusNavigationDirection.First)); + e.Handled = true; + } + } +} diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs new file mode 100644 index 0000000000000..9ea9300756fec --- /dev/null +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Windows.Interop; +using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.VisualStudio.PlatformUI.OleComponentSupport; + +namespace Microsoft.CodeAnalysis.Editor.InlineRename.Adornment +{ + internal class InlineRenameAdornmentViewModel : INotifyPropertyChanged, IDisposable + { + private readonly InlineRenameSession _session; + private OleComponent? _oleComponent; + private bool _disposedValue; + public event PropertyChangedEventHandler? PropertyChanged; + + public InlineRenameAdornmentViewModel(InlineRenameSession session) + { + _session = session; + _session.ReplacementTextChanged += OnReplacementTextChanged; + + _previewChangesFlag = _session.OptionSet.GetOption(RenameOptions.PreviewChanges); + _renameFileFlag = _session.OptionSet.GetOption(RenameOptions.RenameFile); + _renameInStringsFlag = _session.OptionSet.GetOption(RenameOptions.RenameInStrings); + _renameInCommentsFlag = _session.OptionSet.GetOption(RenameOptions.RenameInComments); + _renameOverloadsFlag = _session.OptionSet.GetOption(RenameOptions.RenameOverloads); + + RegisterOleComponent(); + } + + public string IdentifierText + { + get => _session.ReplacementText; + set + { + if (value != _session.ReplacementText) + { + _session.ApplyReplacementText(value, propagateEditImmediately: false); + NotifyPropertyChanged(nameof(IdentifierText)); + } + } + } + + public bool AllowFileRename => _session.FileRenameInfo == InlineRenameFileRenameInfo.Allowed; + public bool ShowFileRename => _session.FileRenameInfo != InlineRenameFileRenameInfo.NotAllowed; + + public string FileRenameString => _session.FileRenameInfo switch + { + InlineRenameFileRenameInfo.TypeDoesNotMatchFileName => EditorFeaturesResources.Rename_file_name_doesnt_match, + InlineRenameFileRenameInfo.TypeWithMultipleLocations => EditorFeaturesResources.Rename_file_partial_type, + _ => EditorFeaturesResources.Rename_symbols_file + }; + + private bool _renameInCommentsFlag; + public bool RenameInCommentsFlag + { + get => _renameInCommentsFlag; + set + { + if (Set(ref _renameInCommentsFlag, value)) + { + _session.RefreshRenameSessionWithOptionsChanged(RenameOptions.RenameInComments, value); + } + } + } + + private bool _renameInStringsFlag; + public bool RenameInStringsFlag + { + get => _renameInStringsFlag; + set + { + if (Set(ref _renameInStringsFlag, value)) + { + _session.RefreshRenameSessionWithOptionsChanged(RenameOptions.RenameInStrings, value); + } + } + } + + private bool _renameFileFlag; + public bool RenameFileFlag + { + get => _renameFileFlag; + set + { + if (Set(ref _renameFileFlag, value)) + { + _session.RefreshRenameSessionWithOptionsChanged(RenameOptions.RenameFile, value); + } + } + } + + private bool _previewChangesFlag; + public bool PreviewChangesFlag + { + get => _previewChangesFlag; + set + { + if (Set(ref _previewChangesFlag, value)) + { + _session.RefreshRenameSessionWithOptionsChanged(RenameOptions.PreviewChanges, value); + } + } + } + + private bool _renameOverloadsFlag; + public bool RenameOverloadsFlag + { + get => _renameOverloadsFlag; + set + { + if (Set(ref _renameOverloadsFlag, value)) + { + _session.RefreshRenameSessionWithOptionsChanged(RenameOptions.RenameOverloads, value); + } + } + } + + public void Submit() + { + _session.Commit(); + } + + public void Cancel() + { + _session.Cancel(); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Shell routes commands based on focused tool window. Since we're outside of a tool window, + /// Editor can end up intercepting commands and TYPECHARs sent to us, even when we're focused, + /// so hook in and intercept each message for WPF. + /// + public void RegisterOleComponent() + { + Debug.Assert(_oleComponent is null); + + _oleComponent = OleComponent.CreateHostedComponent("Microsoft CodeAnalysis Inline Rename"); + _oleComponent.PreTranslateMessage += OnPreTranslateMessage; + _oleComponent.BeginTracking(); + } + + private void UnregisterOleComponent() + { + if (_oleComponent is not null) + { + _oleComponent.EndTracking(); + _oleComponent.PreTranslateMessage -= OnPreTranslateMessage; + _oleComponent.Dispose(); + _oleComponent = null; + } + } + + private void OnPreTranslateMessage(object sender, PreTranslateMessageEventArgs e) + { + var msg = e.Message; + if (ComponentDispatcher.RaiseThreadMessage(ref msg) || IsSuppressedMessage(msg)) + { + e.MessageConsumed = true; + } + + // When the adornment is focused, we register an OleComponent to divert window messages + // away from the editor and back to WPF to enable proper handling of arrows, backspace, + // delete, etc. Unfortunately, anything not handled by WPF is then propagated back to the + // shell command system where it is handled by the open editor window. + // To avoid unhandled arrow commands from being handled by editor, + // we mark them as handled so long as the adornment is focused. + static bool IsSuppressedMessage(MSG msg) + => msg.message switch + { + 0x0100 or // WM_KEYDOWN + 0x0101 // WM_KEYUP + => msg.wParam.ToInt32() switch + { + >= 0x0025 and <= 0x0028 => true, // VK_LEFT, VK_UP, VK_RIGHT, and VK_DOWN + + 0x0021 or // VK_PRIOR (Page Up) + 0x0022 or // VK_NEXT (Page Down) + 0x0023 or // VK_END + 0x0024 or // VK_HOME + 0x0D00 or // VK_RETURN + 0x0009 => true, // VK_TAB + + _ => false + }, + + _ => false + }; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _session.ReplacementTextChanged -= OnReplacementTextChanged; + + UnregisterOleComponent(); + } + + _disposedValue = true; + } + } + + private void OnReplacementTextChanged(object sender, EventArgs e) + { + NotifyPropertyChanged(nameof(IdentifierText)); + } + + private void NotifyPropertyChanged([CallerMemberName] string? name = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + private bool Set(ref T field, T newValue, [CallerMemberName] string? name = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) + { + return false; + } + + field = newValue; + NotifyPropertyChanged(name); + return true; + } + } +} diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/Dashboard/Dashboard.xaml b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/Dashboard.xaml similarity index 96% rename from src/EditorFeatures/Core.Wpf/InlineRename/Dashboard/Dashboard.xaml rename to src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/Dashboard.xaml index 18c0b5beaba69..f0a17489c9676 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/Dashboard/Dashboard.xaml +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/Dashboard.xaml @@ -24,15 +24,15 @@ - + - + + + + + + + + + + + + + + + + + + + diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs index 1f8351593fe92..9138e5550fa4a 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornment.xaml.cs @@ -43,6 +43,7 @@ public InlineRenameAdornment(InlineRenameAdornmentViewModel viewModel, ITextView public string ApplyRename => EditorFeaturesResources.Apply1; public string CancelRename => EditorFeaturesResources.Cancel; public string PreviewChanges => EditorFeaturesResources.Preview_changes1; + public string SubmitText => EditorFeaturesWpfResources.Enter_to_rename_shift_enter_to_preview; #pragma warning restore CA1822 // Mark members as static private void TextView_CursorChanged(object sender, CaretPositionChangedEventArgs e) @@ -99,9 +100,9 @@ private void Adornment_KeyDown(object sender, KeyEventArgs e) case Key.Tab: // We don't want tab to lose focus for the adornment, so manually // loop focus back to the first item that is focusable. - FrameworkElement lastItem = OptionsExpander.IsExpanded - ? PreviewChangesCheckbox - : OptionsExpander; + FrameworkElement lastItem = _viewModel.IsExpanded + ? FileRenameCheckbox + : IdentifierTextBox; if (lastItem.IsFocused) { @@ -125,8 +126,18 @@ private void Adornment_ConsumeMouseEvent(object sender, MouseButtonEventArgs e) private void Adornment_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) { - MoveFocus(new TraversalRequest(FocusNavigationDirection.First)); + if (e.OldFocus == this) + { + return; + } + + IdentifierTextBox.Focus(); e.Handled = true; } + + private void ToggleExpand(object sender, RoutedEventArgs e) + { + _viewModel.IsExpanded = !_viewModel.IsExpanded; + } } } diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs index c1018ea78e4c1..c9932ebf66ef6 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/InlineRenameAdornmentViewModel.cs @@ -130,6 +130,28 @@ public bool RenameOverloadsFlag } } + private bool _isCollapsed; + public bool IsCollapsed + { + get => _isCollapsed; + set + { + if (Set(ref _isCollapsed, value)) + { + NotifyPropertyChanged(nameof(IsExpanded)); + } + } + } + + public bool IsExpanded + { + get => !IsCollapsed; + set => IsCollapsed = !value; + } + + public bool IsRenameOverloadsEditable + => !_session.MustRenameOverloads; + public void Submit() { _session.Commit(); diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameAdornmentManager.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameAdornmentManager.cs index 3a04f0ea4e449..2b7ba9ad31710 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameAdornmentManager.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameAdornmentManager.cs @@ -70,7 +70,6 @@ private void UpdateAdornments() _dashboardColorUpdater?.UpdateColors(); var useInlineAdornment = _globalOptionService.GetOption(InlineRenameExperimentationOptions.UseInlineAdornment); - if (useInlineAdornment) { var adornment = new InlineRenameAdornment( @@ -94,7 +93,6 @@ private void UpdateAdornments() _adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, newAdornment, (tag, adornment) => ((Dashboard)adornment).Dispose()); } -#pragma warning restore CS0162 // Unreachable code detected } } diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.cs index f833aa7893d9b..b72746bed7485 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.cs @@ -20,5 +20,6 @@ internal static class InlineRenameColors public static object BackgroundBrushKey { get; set; } = "BackgroundBrush"; public static object AccentBarColorKey { get; set; } = "AccentBarBrush"; public static object ButtonStyleKey { get; set; } = "ButtonStyle"; + public static object ButtonBorderBrush { get; set; } = "ButtonBorderBrush"; } } diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.xaml b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.xaml index aac0a4e806db8..bc31b825694a9 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.xaml +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/InlineRenameColors.xaml @@ -11,5 +11,6 @@ LightGray Blue + White