/* 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 . */ 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(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? 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(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() { } } }