Dnspy/dnSpy/dnSpy.Contracts.DnSpy/Controls/ToolWindows/EditValueProviderService.cs
2021-09-20 18:20:01 +02:00

254 lines
10 KiB
C#

/*
Copyright (C) 2014-2019 de4dot@gmail.com
This file is part of dnSpy
dnSpy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
dnSpy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Controls;
using dnSpy.Contracts.Command;
using dnSpy.Contracts.Settings.AppearanceCategory;
using dnSpy.Contracts.Text.Editor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace dnSpy.Contracts.Controls.ToolWindows {
abstract class EditValueProviderService {
public abstract IEditValueProvider Create(string contentType, string[] extraTextViewRoles);
}
[Export(typeof(EditValueProviderService))]
sealed class EditValueProviderServiceImpl : EditValueProviderService {
readonly IContentTypeRegistryService contentTypeRegistryService;
readonly ITextBufferFactoryService textBufferFactoryService;
readonly ITextEditorFactoryService textEditorFactoryService;
[ImportingConstructor]
EditValueProviderServiceImpl(IContentTypeRegistryService contentTypeRegistryService, ITextBufferFactoryService textBufferFactoryService, ITextEditorFactoryService textEditorFactoryService) {
this.contentTypeRegistryService = contentTypeRegistryService;
this.textBufferFactoryService = textBufferFactoryService;
this.textEditorFactoryService = textEditorFactoryService;
}
public override IEditValueProvider Create(string contentType, string[] extraTextViewRoles) {
if (contentType is null)
throw new ArgumentNullException(nameof(contentType));
if (extraTextViewRoles is null)
throw new ArgumentNullException(nameof(extraTextViewRoles));
var ct = contentTypeRegistryService.GetContentType(contentType);
if (ct is null)
throw new ArgumentOutOfRangeException(nameof(contentType));
return new EditValueProviderImpl(ct, textBufferFactoryService, textEditorFactoryService, extraTextViewRoles);
}
}
static class EditValueConstants {
public const string EditValueTextViewRole = nameof(EditValueTextViewRole);
}
sealed class EditValueProviderImpl : IEditValueProvider {
readonly IContentType contentType;
readonly ITextBufferFactoryService textBufferFactoryService;
readonly ITextEditorFactoryService textEditorFactoryService;
readonly string[] extraTextViewRoles;
public EditValueProviderImpl(IContentType contentType, ITextBufferFactoryService textBufferFactoryService, ITextEditorFactoryService textEditorFactoryService, string[] extraTextViewRoles) {
this.contentType = contentType ?? throw new ArgumentNullException(nameof(contentType));
this.textBufferFactoryService = textBufferFactoryService ?? throw new ArgumentNullException(nameof(textBufferFactoryService));
this.textEditorFactoryService = textEditorFactoryService ?? throw new ArgumentNullException(nameof(textEditorFactoryService));
this.extraTextViewRoles = extraTextViewRoles ?? throw new ArgumentNullException(nameof(extraTextViewRoles));
}
public IEditValue Create(string text, EditValueFlags flags) {
var buffer = textBufferFactoryService.CreateTextBuffer(text, contentType);
var rolesHash = new HashSet<string>(textEditorFactoryService.DefaultRoles, StringComparer.OrdinalIgnoreCase) {
EditValueConstants.EditValueTextViewRole,
};
// This also disables: line compressor, current line highlighter
rolesHash.Remove(PredefinedTextViewRoles.Document);
rolesHash.Remove(PredefinedTextViewRoles.Zoomable);
foreach (var s in extraTextViewRoles)
rolesHash.Add(s);
var roles = textEditorFactoryService.CreateTextViewRoleSet(rolesHash);
var textView = textEditorFactoryService.CreateTextView(buffer, roles);
try {
return new EditValueImpl(textView, flags);
}
catch {
textView.Close();
throw;
}
}
}
sealed class EditValueImpl : IEditValue {
public event EventHandler<EditCompletedEventArgs>? EditCompleted;
public object? UIObject => uiControl;
public bool IsKeyboardFocused => wpfTextView.HasAggregateFocus;
sealed class UIControl : ContentControl {
readonly IWpfTextView wpfTextView;
double lastHeight;
public UIControl(IWpfTextView wpfTextView) {
this.wpfTextView = wpfTextView ?? throw new ArgumentNullException(nameof(wpfTextView));
wpfTextView.LayoutChanged += WpfTextView_LayoutChanged;
Content = wpfTextView.VisualElement;
}
void WpfTextView_LayoutChanged(object? sender, TextViewLayoutChangedEventArgs e) {
var height = wpfTextView.TextViewLines[0].Height;
if (height != lastHeight) {
lastHeight = height;
InvalidateMeasure();
}
}
protected override Size MeasureOverride(Size constraint) {
var res = base.MeasureOverride(constraint);
return new Size(res.Width, Math.Max(res.Height, lastHeight));
}
internal void Dispose() => wpfTextView.LayoutChanged -= WpfTextView_LayoutChanged;
}
readonly IWpfTextView wpfTextView;
readonly UIControl uiControl;
readonly EditValueFlags flags;
public EditValueImpl(IWpfTextView wpfTextView, EditValueFlags flags) {
this.wpfTextView = wpfTextView;
uiControl = new UIControl(wpfTextView);
this.flags = flags;
wpfTextView.VisualElement.Loaded += VisualElement_Loaded;
wpfTextView.TextBuffer.Properties.AddProperty(typeof(EditValueImpl), this);
wpfTextView.Options.SetOptionValue(DefaultOptions.ConvertTabsToSpacesOptionId, true);
wpfTextView.Options.SetOptionValue(DefaultTextViewOptions.WordWrapStyleId, WordWrapStyles.None);
wpfTextView.Options.SetOptionValue(DefaultWpfViewOptions.EnableHighlightCurrentLineId, false);
wpfTextView.Options.SetOptionValue(DefaultWpfViewOptions.EnableMouseWheelZoomId, false);
wpfTextView.Options.SetOptionValue(DefaultWpfViewOptions.AppearanceCategory, AppearanceCategoryConstants.UIMisc);
wpfTextView.Options.SetOptionValue(DefaultDsTextViewOptions.CanChangeWordWrapStyleId, false);
wpfTextView.Options.SetOptionValue(DefaultDsTextViewOptions.CompressEmptyOrWhitespaceLinesId, false);
wpfTextView.Options.SetOptionValue(DefaultDsTextViewOptions.CompressNonLetterLinesId, false);
}
public static EditValueImpl TryGetInstance(ITextView textView) {
textView.TextBuffer.Properties.TryGetProperty<EditValueImpl>(typeof(EditValueImpl), out var instance);
return instance;
}
void VisualElement_Loaded(object? sender, RoutedEventArgs e) {
wpfTextView.VisualElement.Loaded -= VisualElement_Loaded;
wpfTextView.VisualElement.Focus();
var snapshot = wpfTextView.TextSnapshot;
if ((flags & EditValueFlags.SelectText) != 0)
wpfTextView.Selection.Select(new SnapshotSpan(snapshot, new Span(0, snapshot.Length)), isReversed: false);
wpfTextView.Caret.MoveTo(new SnapshotPoint(snapshot, snapshot.Length));
wpfTextView.Caret.EnsureVisible();
wpfTextView.LostAggregateFocus += WpfTextView_LostAggregateFocus;
}
void WpfTextView_LostAggregateFocus(object? sender, EventArgs e) => Cancel();
public void Cancel() => OnEditCompleted(null);
public void Commit() => OnEditCompleted(wpfTextView.TextBuffer.CurrentSnapshot.GetText());
void OnEditCompleted(string? text) {
EditCompleted?.Invoke(this, new EditCompletedEventArgs(text));
Dispose();
}
public void Dispose() {
if (wpfTextView.IsClosed)
return;
uiControl.Dispose();
wpfTextView.Properties.RemoveProperty(typeof(EditValueImpl));
wpfTextView.VisualElement.Loaded -= VisualElement_Loaded;
wpfTextView.LostAggregateFocus -= WpfTextView_LostAggregateFocus;
wpfTextView.Close();
}
}
[ExportCommandTargetFilterProvider(CommandTargetFilterOrder.TextEditor - 100)]
sealed class EditValueCommandTargetFilterProvider : ICommandTargetFilterProvider {
public ICommandTargetFilter? Create(object target) {
var textView = target as ITextView;
if (textView?.Roles.Contains(EditValueConstants.EditValueTextViewRole) != true)
return null;
return new EditValueCommandTargetFilter(textView);
}
}
sealed class EditValueCommandTargetFilter : ICommandTargetFilter {
readonly ITextView textView;
EditValueImpl TryGetInstance() => __editValueImpl ??= EditValueImpl.TryGetInstance(textView);
EditValueImpl? __editValueImpl;
public EditValueCommandTargetFilter(ITextView textView) => this.textView = textView;
public CommandTargetStatus CanExecute(Guid group, int cmdId) {
if (TryGetInstance() is null)
return CommandTargetStatus.NotHandled;
if (group == CommandConstants.TextEditorGroup) {
switch ((TextEditorIds)cmdId) {
case TextEditorIds.CANCEL:
case TextEditorIds.RETURN:
return CommandTargetStatus.Handled;
default:
return CommandTargetStatus.NotHandled;
}
}
return CommandTargetStatus.NotHandled;
}
public CommandTargetStatus Execute(Guid group, int cmdId, object? args = null) {
object? result = null;
return Execute(group, cmdId, args, ref result);
}
public CommandTargetStatus Execute(Guid group, int cmdId, object? args, ref object? result) {
var editValueImpl = TryGetInstance();
if (editValueImpl is null)
return CommandTargetStatus.NotHandled;
if (group == CommandConstants.TextEditorGroup) {
switch ((TextEditorIds)cmdId) {
case TextEditorIds.CANCEL:
editValueImpl.Cancel();
return CommandTargetStatus.Handled;
case TextEditorIds.RETURN:
editValueImpl.Commit();
return CommandTargetStatus.Handled;
default:
return CommandTargetStatus.NotHandled;
}
}
return CommandTargetStatus.NotHandled;
}
public void SetNextCommandTarget(ICommandTarget commandTarget) { }
public void Dispose() { }
}
}