From 080f1697e97e13461ec6df4d31c8924d01257a1b Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Tue, 9 Apr 2019 01:47:48 +0300 Subject: MERGE --- .../CodeCompletion/CompletionList.cs | 377 +++++++++++++++++++ .../CodeCompletion/CompletionList.xaml | 72 ++++ .../CodeCompletion/CompletionListBox.cs | 103 +++++ .../CodeCompletion/CompletionListBoxItem.cs | 63 ++++ .../CodeCompletion/CompletionWindow.cs | 196 ++++++++++ .../CodeCompletion/CompletionWindowBase.cs | 417 +++++++++++++++++++++ .../CodeCompletion/ICompletionData.cs | 49 +++ .../CodeCompletion/IOverloadProvider.cs | 42 +++ .../CodeCompletion/InsightWindow.cs | 94 +++++ .../CodeCompletion/InsightWindow.xaml | 118 ++++++ .../CodeCompletion/OverloadInsightWindow.cs | 58 +++ .../CodeCompletion/OverloadViewer.cs | 101 +++++ 12 files changed, 1690 insertions(+) create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.xaml create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBox.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBoxItem.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindow.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindowBase.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/ICompletionData.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/IOverloadProvider.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.xaml create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadInsightWindow.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadViewer.cs (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion') diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.cs new file mode 100644 index 000000000..ea6e1856f --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.cs @@ -0,0 +1,377 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Input; +using System.Linq; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// The listbox used inside the CompletionWindow, contains CompletionListBox. + /// + public class CompletionList : Control + { + static CompletionList() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList), + new FrameworkPropertyMetadata(typeof(CompletionList))); + } + + bool isFiltering = true; + /// + /// If true, the CompletionList is filtered to show only matching items. Also enables search by substring. + /// If false, enables the old behavior: no filtering, search by string.StartsWith. + /// + public bool IsFiltering { + get { return isFiltering; } + set { isFiltering = value; } + } + + /// + /// Dependency property for . + /// + public static readonly DependencyProperty EmptyTemplateProperty = + DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList), + new FrameworkPropertyMetadata()); + + /// + /// Content of EmptyTemplate will be shown when CompletionList contains no items. + /// If EmptyTemplate is null, nothing will be shown. + /// + public ControlTemplate EmptyTemplate { + get { return (ControlTemplate)GetValue(EmptyTemplateProperty); } + set { SetValue(EmptyTemplateProperty, value); } + } + + /// + /// Is raised when the completion list indicates that the user has chosen + /// an entry to be completed. + /// + public event EventHandler InsertionRequested; + + /// + /// Raises the InsertionRequested event. + /// + public void RequestInsertion(EventArgs e) + { + if (InsertionRequested != null) + InsertionRequested(this, e); + } + + CompletionListBox listBox; + + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + listBox = GetTemplateChild("PART_ListBox") as CompletionListBox; + if (listBox != null) { + listBox.ItemsSource = completionData; + } + } + + /// + /// Gets the list box. + /// + public CompletionListBox ListBox { + get { + if (listBox == null) + ApplyTemplate(); + return listBox; + } + } + + /// + /// Gets the scroll viewer used in this list box. + /// + public ScrollViewer ScrollViewer { + get { return listBox != null ? listBox.scrollViewer : null; } + } + + ObservableCollection completionData = new ObservableCollection(); + + /// + /// Gets the list to which completion data can be added. + /// + public IList CompletionData { + get { return completionData; } + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled) { + HandleKey(e); + } + } + + /// + /// Handles a key press. Used to let the completion list handle key presses while the + /// focus is still on the text editor. + /// + public void HandleKey(KeyEventArgs e) + { + if (listBox == null) + return; + + // We have to do some key handling manually, because the default doesn't work with + // our simulated events. + // Also, the default PageUp/PageDown implementation changes the focus, so we avoid it. + switch (e.Key) { + case Key.Down: + e.Handled = true; + listBox.SelectIndex(listBox.SelectedIndex + 1); + break; + case Key.Up: + e.Handled = true; + listBox.SelectIndex(listBox.SelectedIndex - 1); + break; + case Key.PageDown: + e.Handled = true; + listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount); + break; + case Key.PageUp: + e.Handled = true; + listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount); + break; + case Key.Home: + e.Handled = true; + listBox.SelectIndex(0); + break; + case Key.End: + e.Handled = true; + listBox.SelectIndex(listBox.Items.Count - 1); + break; + case Key.Tab: + case Key.Enter: + e.Handled = true; + RequestInsertion(e); + break; + } + } + + /// + protected override void OnMouseDoubleClick(MouseButtonEventArgs e) + { + base.OnMouseDoubleClick(e); + if (e.ChangedButton == MouseButton.Left) { + e.Handled = true; + RequestInsertion(e); + } + } + + /// + /// Gets/Sets the selected item. + /// + public ICompletionData SelectedItem { + get { + return (listBox != null ? listBox.SelectedItem : null) as ICompletionData; + } + set { + if (listBox == null && value != null) + ApplyTemplate(); + listBox.SelectedItem = value; + } + } + + /// + /// Occurs when the SelectedItem property changes. + /// + public event SelectionChangedEventHandler SelectionChanged { + add { AddHandler(Selector.SelectionChangedEvent, value); } + remove { RemoveHandler(Selector.SelectionChangedEvent, value); } + } + + // SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once + string currentText; + ObservableCollection currentList; + + /// + /// Selects the best match, and filter the items if turned on using . + /// + public void SelectItem(string text) + { + if (text == currentText) + return; + if (listBox == null) + ApplyTemplate(); + + if (this.IsFiltering) { + SelectItemFiltering(text); + } + else { + SelectItemWithStart(text); + } + currentText = text; + } + + /// + /// Filters CompletionList items to show only those matching given query, and selects the best match. + /// + public void SelectItemFiltering(string query) + { + // if the user just typed one more character, don't filter all data but just filter what we are already displaying + var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) && + query.StartsWith(this.currentText, StringComparison.Ordinal)) ? + this.currentList : this.completionData; + + var matchingItems = + from item in listToFilter + let quality = GetMatchQuality(item.Text, query) + where quality > 0 + select new { Item = item, Quality = quality }; + + // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" + ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null; + + var listBoxItems = new ObservableCollection(); + int bestIndex = -1; + int bestQuality = -1; + double bestPriority = 0; + int i = 0; + foreach (var matchingItem in matchingItems) { + double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority; + int quality = matchingItem.Quality; + if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } + listBoxItems.Add(matchingItem.Item); + i++; + } + this.currentList = listBoxItems; + listBox.ItemsSource = listBoxItems; + SelectIndexCentered(bestIndex); + } + + /// + /// Selects the item that starts with the specified query. + /// + void SelectItemWithStart(string query) + { + if (string.IsNullOrEmpty(query)) + return; + + int suggestedIndex = listBox.SelectedIndex; + + int bestIndex = -1; + int bestQuality = -1; + double bestPriority = 0; + for (int i = 0; i < completionData.Count; ++i) { + int quality = GetMatchQuality(completionData[i].Text, query); + if (quality < 0) + continue; + + double priority = completionData[i].Priority; + bool useThisItem; + if (bestQuality < quality) { + useThisItem = true; + } else { + if (bestIndex == suggestedIndex) { + useThisItem = false; + } else if (i == suggestedIndex) { + // prefer recommendedItem, regardless of its priority + useThisItem = bestQuality == quality; + } else { + useThisItem = bestQuality == quality && bestPriority < priority; + } + } + if (useThisItem) { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } + } + SelectIndexCentered(bestIndex); + } + + void SelectIndexCentered(int bestIndex) + { + if (bestIndex < 0) { + listBox.ClearSelection(); + } else { + int firstItem = listBox.FirstVisibleItem; + if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) { + // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null + listBox.CenterViewOn(bestIndex); + listBox.SelectIndex(bestIndex); + } else { + listBox.SelectIndex(bestIndex); + } + } + } + + int GetMatchQuality(string itemText, string query) + { + if (itemText == null) + throw new ArgumentNullException("itemText", "ICompletionData.Text returned null"); + + // Qualities: + // 8 = full match case sensitive + // 7 = full match + // 6 = match start case sensitive + // 5 = match start + // 4 = match CamelCase when length of query is 1 or 2 characters + // 3 = match substring case sensitive + // 2 = match sustring + // 1 = match CamelCase + // -1 = no match + if (query == itemText) + return 8; + if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase)) + return 7; + + if (itemText.StartsWith(query, StringComparison.InvariantCulture)) + return 6; + if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase)) + return 5; + + bool? camelCaseMatch = null; + if (query.Length <= 2) { + camelCaseMatch = CamelCaseMatch(itemText, query); + if (camelCaseMatch == true) return 4; + } + + // search by substring, if filtering (i.e. new behavior) turned on + if (IsFiltering) { + if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0) + return 3; + if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0) + return 2; + } + + if (!camelCaseMatch.HasValue) + camelCaseMatch = CamelCaseMatch(itemText, query); + if (camelCaseMatch == true) + return 1; + + return -1; + } + + static bool CamelCaseMatch(string text, string query) + { + int i = 0; + foreach (char upper in text.Where(c => char.IsUpper(c))) { + if (i > query.Length - 1) + return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis") + if (char.ToUpper(query[i], CultureInfo.InvariantCulture) != upper) + return false; + i++; + } + if (i >= query.Length) + return true; + return false; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.xaml b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.xaml new file mode 100644 index 000000000..3887b139d --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionList.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBox.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBox.cs new file mode 100644 index 000000000..235c07304 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBox.cs @@ -0,0 +1,103 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Controls; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// The list box used inside the CompletionList. + /// + public class CompletionListBox : ListBox + { + internal ScrollViewer scrollViewer; + + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Find the scroll viewer: + scrollViewer = null; + if (this.VisualChildrenCount > 0) { + Border border = this.GetVisualChild(0) as Border; + if (border != null) + scrollViewer = border.Child as ScrollViewer; + scrollViewer.BorderThickness = new Thickness(0); + + } + } + + /// + /// Gets the number of the first visible item. + /// + public int FirstVisibleItem { + get { + if (scrollViewer == null || scrollViewer.ExtentHeight == 0) { + return 0; + } else { + return (int)(this.Items.Count * scrollViewer.VerticalOffset / scrollViewer.ExtentHeight); + } + } + set { + value = value.CoerceValue(0, this.Items.Count - this.VisibleItemCount); + if (scrollViewer != null) { + scrollViewer.ScrollToVerticalOffset((double)value / this.Items.Count * scrollViewer.ExtentHeight); + } + } + } + + /// + /// Gets the number of visible items. + /// + public int VisibleItemCount { + get { + if (scrollViewer == null || scrollViewer.ExtentHeight == 0) { + return 10; + } else { + return Math.Max( + 3, + (int)Math.Ceiling(this.Items.Count * scrollViewer.ViewportHeight + / scrollViewer.ExtentHeight)); + } + } + } + + /// + /// Removes the selection. + /// + public void ClearSelection() + { + this.SelectedIndex = -1; + } + + /// + /// Selects the item with the specified index and scrolls it into view. + /// + public void SelectIndex(int index) + { + if (index >= this.Items.Count) + index = this.Items.Count - 1; + if (index < 0) + index = 0; + this.SelectedIndex = index; + this.ScrollIntoView(this.SelectedItem); + } + + /// + /// Centers the view on the item with the specified index. + /// + public void CenterViewOn(int index) + { + this.FirstVisibleItem = index - VisibleItemCount / 2; + } + + protected override DependencyObject GetContainerForItemOverride() + { + return new CompletionListBoxItem(); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBoxItem.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBoxItem.cs new file mode 100644 index 000000000..55d752bfd --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionListBoxItem.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + public class CompletionListBoxItem : ListBoxItem + { + public ToolTip toolTip; + + public DataTemplate ToolTipContentTemplate + { + get { return (DataTemplate)GetValue(ToolTipContentTemplateProperty); } + set { SetValue(ToolTipContentTemplateProperty, value); } + } + public static readonly DependencyProperty ToolTipContentTemplateProperty = + DependencyProperty.Register("ToolTipContentTemplate", typeof(DataTemplate), typeof(CompletionListBoxItem), new PropertyMetadata(null)); + + public CompletionListBoxItem() + { + toolTip = new ToolTip(); + toolTip.PlacementTarget = this; + toolTip.HorizontalOffset = 15; + toolTip.Background = Brushes.Transparent; + toolTip.BorderThickness = new Thickness(0); + toolTip.Opacity = 1; + toolTip.HasDropShadow = true; + toolTip.Placement = System.Windows.Controls.Primitives.PlacementMode.Right; + this.Unloaded += CompletionListBoxItem_Unloaded; + } + + private void CompletionListBoxItem_Unloaded(object sender, RoutedEventArgs e) + { + toolTip.StaysOpen = false; + toolTip.IsOpen = false; + } + + protected override void OnSelected(RoutedEventArgs e) + { + try + { + base.OnSelected(e); + toolTip.Content = DataContext; + toolTip.ContentTemplate = ToolTipContentTemplate; + toolTip.StaysOpen = true; + toolTip.IsOpen = true; + } + catch { } + } + + protected override void OnUnselected(RoutedEventArgs e) + { + base.OnUnselected(e); + toolTip.StaysOpen = false; + toolTip.IsOpen = false; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindow.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindow.cs new file mode 100644 index 000000000..33759e351 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindow.cs @@ -0,0 +1,196 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Threading; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using System.Windows.Media; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// The code completion window. + /// + public class CompletionWindow : CompletionWindowBase + { + readonly CompletionList completionList = new CompletionList(); + + public event Action InsertionRequest; + + /// + /// Gets the completion list used in this completion window. + /// + public CompletionList CompletionList + { + get { return completionList; } + } + + /// + /// Creates a new code completion window. + /// + public CompletionWindow(TextArea textArea) : base(textArea) + { + + // keep height automatic + this.CloseAutomatically = true; + this.SizeToContent = SizeToContent.WidthAndHeight; + this.WindowStyle = WindowStyle.None; + this.ResizeMode = ResizeMode.NoResize; + this.Content = completionList; + // prevent user from resizing window to 0x0 + this.MinHeight = 15; + this.MinWidth = 30; + //this.Background = new SolidColorBrush(Color.FromRgb(15, 15, 15)); + this.Foreground = Brushes.Gainsboro; + } + + public override void ShowCompletion() + { + base.ShowCompletion(); + AttachEvents(); + } + + public override void HideCompletion() + { + base.HideCompletion(); + + foreach (var item in completionList.ListBox.Items) + { + var box = completionList.ListBox.ItemContainerGenerator.ContainerFromItem(item) as CompletionListBoxItem; + if (box != null && box.IsSelected) + { + box.toolTip.IsOpen = false; + } + } + } + + void completionList_InsertionRequested(object sender, EventArgs e) + { + HideCompletion(); + // The window must close before Complete() is called. + // If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. + var item = completionList.SelectedItem; + //if (item != null) + // item.Complete(this.TextArea, new AnchorSegment(this.TextArea.Document, this.StartOffset, this.EndOffset - this.StartOffset), e); + + if (item != null) + { + InsertionRequest?.Invoke(item); + } + + completionList.SelectedItem = null; + } + + void AttachEvents() + { + this.completionList.InsertionRequested += completionList_InsertionRequested; + this.TextArea.Caret.PositionChanged += CaretPositionChanged; + this.TextArea.MouseWheel += textArea_MouseWheel; + this.TextArea.PreviewTextInput += textArea_PreviewTextInput; + } + + /// + protected override void DetachEvents() + { + this.completionList.InsertionRequested -= completionList_InsertionRequested; + this.TextArea.Caret.PositionChanged -= CaretPositionChanged; + this.TextArea.MouseWheel -= textArea_MouseWheel; + this.TextArea.PreviewTextInput -= textArea_PreviewTextInput; + base.DetachEvents(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled) + { + completionList.HandleKey(e); + } + } + + void textArea_PreviewTextInput(object sender, TextCompositionEventArgs e) + { + e.Handled = RaiseEventPair(this, PreviewTextInputEvent, TextInputEvent, + new TextCompositionEventArgs(e.Device, e.TextComposition)); + + if (e.Text == " ") + { + HideCompletion(); + } + } + + void textArea_MouseWheel(object sender, MouseWheelEventArgs e) + { + e.Handled = RaiseEventPair(GetScrollEventTarget(), + PreviewMouseWheelEvent, MouseWheelEvent, + new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)); + } + + UIElement GetScrollEventTarget() + { + if (completionList == null) + return this; + return completionList.ScrollViewer ?? completionList.ListBox ?? (UIElement)completionList; + } + + /// + /// Gets/Sets whether the completion window should close automatically. + /// The default value is true. + /// + public bool CloseAutomatically { get; set; } + + /// + protected override bool CloseOnFocusLost + { + get { return this.CloseAutomatically; } + } + + /// + /// When this flag is set, code completion closes if the caret moves to the + /// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing", + /// but not in dot-completion. + /// Has no effect if CloseAutomatically is false. + /// + public bool CloseWhenCaretAtBeginning { get; set; } + + void CaretPositionChanged(object sender, EventArgs e) + { + int offset = this.TextArea.Caret.Offset; + if (offset == this.StartOffset) + { + if (CloseAutomatically && CloseWhenCaretAtBeginning) + { + HideCompletion(); + } + else + { + completionList.SelectItem(string.Empty); + } + return; + } + if (offset < this.StartOffset || offset > this.EndOffset) + { + if (CloseAutomatically) + { + HideCompletion(); + } + } + else + { + TextDocument document = this.TextArea.Document; + if (document != null) + { + completionList.SelectItem(document.GetText(this.StartOffset, offset - this.StartOffset)); + } + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindowBase.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindowBase.cs new file mode 100644 index 000000000..7766d323a --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/CompletionWindowBase.cs @@ -0,0 +1,417 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Threading; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; +using System.ComponentModel; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// Base class for completion windows. Handles positioning the window at the caret. + /// + public class CompletionWindowBase : Window + { + static CompletionWindowBase() + { + WindowStyleProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(WindowStyle.None)); + ShowActivatedProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); + ShowInTaskbarProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); + } + + /// + /// Gets the parent TextArea. + /// + public TextArea TextArea { get; private set; } + + Window parentWindow; + TextDocument document; + + /// + /// Gets/Sets the start of the text range in which the completion window stays open. + /// This text portion is used to determine the text used to select an entry in the completion list by typing. + /// + public int StartOffset { get; set; } + + /// + /// Gets/Sets the end of the text range in which the completion window stays open. + /// This text portion is used to determine the text used to select an entry in the completion list by typing. + /// + public int EndOffset { get; set; } + + /// + /// Gets whether the window was opened above the current line. + /// + protected bool IsUp { get; private set; } + + /// + /// Creates a new CompletionWindowBase. + /// + public CompletionWindowBase(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.TextArea = textArea; + + parentWindow = Window.GetWindow(TextArea); + this.Owner = parentWindow; + this.AddHandler(MouseUpEvent, new MouseButtonEventHandler(OnMouseUp), true); + } + + public virtual void ShowCompletion() + { + StartOffset = EndOffset = this.TextArea.Caret.Offset; + + AttachEvents(); + SetPosition(); + UpdatePosition(); + Show(); + } + + public virtual void HideCompletion() + { + Debug.WriteLine("Hide Completion..."); + DetachEvents(); + Hide(); + } + + #region Event Handlers + + void AttachEvents() + { + document = this.TextArea.Document; + if (document != null) { + document.Changing += textArea_Document_Changing; + } + // LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729 + this.TextArea.LostKeyboardFocus += TextAreaLostFocus; + this.TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged; + this.TextArea.DocumentChanged += TextAreaDocumentChanged; + if (parentWindow != null) { + parentWindow.LocationChanged += parentWindow_LocationChanged; + } + + // close previous completion windows of same type + foreach (InputHandler x in this.TextArea.StackedInputHandlers.OfType()) { + if (x.window.GetType() == this.GetType()) + this.TextArea.PopStackedInputHandler(x); + } + + myInputHandler = new InputHandler(this); + this.TextArea.PushStackedInputHandler(myInputHandler); + } + + /// + /// Detaches events from the text area. + /// + protected virtual void DetachEvents() + { + if (document != null) { + document.Changing -= textArea_Document_Changing; + } + this.TextArea.LostKeyboardFocus -= TextAreaLostFocus; + this.TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged; + this.TextArea.DocumentChanged -= TextAreaDocumentChanged; + if (parentWindow != null) { + parentWindow.LocationChanged -= parentWindow_LocationChanged; + } + this.TextArea.PopStackedInputHandler(myInputHandler); + } + + #region InputHandler + InputHandler myInputHandler; + + /// + /// A dummy input handler (that justs invokes the default input handler). + /// This is used to ensure the completion window closes when any other input handler + /// becomes active. + /// + sealed class InputHandler : TextAreaStackedInputHandler + { + internal readonly CompletionWindowBase window; + + public InputHandler(CompletionWindowBase window) + : base(window.TextArea) + { + Debug.Assert(window != null); + this.window = window; + } + + public override void Detach() + { + base.Detach(); + //window.HideCompletion(); + } + + const Key KeyDeadCharProcessed = (Key)0xac; // Key.DeadCharProcessed; // new in .NET 4 + + public override void OnPreviewKeyDown(KeyEventArgs e) + { + // prevents crash when typing deadchar while CC window is open + if (e.Key == KeyDeadCharProcessed) + return; + e.Handled = RaiseEventPair(window, PreviewKeyDownEvent, KeyDownEvent, + new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); + } + + public override void OnPreviewKeyUp(KeyEventArgs e) + { + if (e.Key == KeyDeadCharProcessed) + return; + e.Handled = RaiseEventPair(window, PreviewKeyUpEvent, KeyUpEvent, + new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); + } + } + #endregion + + void TextViewScrollOffsetChanged(object sender, EventArgs e) + { + // Workaround for crash #1580 (reproduction steps unknown): + // NullReferenceException in System.Windows.Window.CreateSourceWindow() + if (!sourceIsInitialized) + return; + + IScrollInfo scrollInfo = this.TextArea.TextView; + Rect visibleRect = new Rect(scrollInfo.HorizontalOffset, scrollInfo.VerticalOffset, scrollInfo.ViewportWidth, scrollInfo.ViewportHeight); + // close completion window when the user scrolls so far that the anchor position is leaving the visible area + if (visibleRect.Contains(visualLocation) || visibleRect.Contains(visualLocationTop)) + UpdatePosition(); + else + HideCompletion(); + + } + + void TextAreaDocumentChanged(object sender, EventArgs e) + { + HideCompletion(); + } + + void TextAreaLostFocus(object sender, RoutedEventArgs e) + { + Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); + } + + void parentWindow_LocationChanged(object sender, EventArgs e) + { + UpdatePosition(); + } + + /// + protected override void OnDeactivated(EventArgs e) + { + base.OnDeactivated(e); + Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); + } + #endregion + + /// + /// Raises a tunnel/bubble event pair for a WPF control. + /// + /// The WPF control for which the event should be raised. + /// The tunneling event. + /// The bubbling event. + /// The event args to use. + /// The value of the event args. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] + protected static bool RaiseEventPair(UIElement target, RoutedEvent previewEvent, RoutedEvent @event, RoutedEventArgs args) + { + if (target == null) + throw new ArgumentNullException("target"); + if (previewEvent == null) + throw new ArgumentNullException("previewEvent"); + if (@event == null) + throw new ArgumentNullException("event"); + if (args == null) + throw new ArgumentNullException("args"); + args.RoutedEvent = previewEvent; + target.RaiseEvent(args); + args.RoutedEvent = @event; + target.RaiseEvent(args); + return args.Handled; + } + + // Special handler: handledEventsToo + void OnMouseUp(object sender, MouseButtonEventArgs e) + { + ActivateParentWindow(); + } + + /// + /// Activates the parent window. + /// + protected virtual void ActivateParentWindow() + { + if (parentWindow != null) + parentWindow.Activate(); + } + + void CloseIfFocusLost() + { + if (CloseOnFocusLost) { + Debug.WriteLine("CloseIfFocusLost: this.IsActive=" + this.IsActive + " IsTextAreaFocused=" + IsTextAreaFocused); + if (!this.IsActive && !IsTextAreaFocused) { + HideCompletion(); + } + } + } + + /// + /// Gets whether the completion window should automatically close when the text editor looses focus. + /// + protected virtual bool CloseOnFocusLost { + get { return true; } + } + + bool IsTextAreaFocused { + get { + if (parentWindow != null && !parentWindow.IsActive) + return false; + return this.TextArea.IsKeyboardFocused; + } + } + + bool sourceIsInitialized; + + /// + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + SetPosition(); + + //sourceIsInitialized = true; + } + + public void SetPosition() + { + if (document != null && this.StartOffset != this.TextArea.Caret.Offset) + { + SetPosition(new TextViewPosition(document.GetLocation(this.StartOffset))); + } + else + { + SetPosition(this.TextArea.Caret.Position); + } + } + + public void UpdatePositionFix() + { + SetPosition(this.TextArea.Caret.Position); + } + + //public void UpdatePosition() + //{ + // SetPosition(this.TextArea.Caret.Position); + //} + + /// + protected override void OnClosed(EventArgs e) + { + DetachEvents(); + HideCompletion(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled && e.Key == Key.Escape) { + e.Handled = true; + HideCompletion(); + } + } + + Point visualLocation, visualLocationTop; + + /// + /// Positions the completion window at the specified position. + /// + protected void SetPosition(TextViewPosition position) + { + TextView textView = this.TextArea.TextView; + + visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom); + visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop); + UpdatePosition(); + } + + /// + /// Updates the position of the CompletionWindow based on the parent TextView position and the screen working area. + /// It ensures that the CompletionWindow is completely visible on the screen. + /// + public void UpdatePosition() + { + TextView textView = this.TextArea.TextView; + // PointToScreen returns device dependent units (physical pixels) + Point location = textView.PointToScreen(visualLocation - textView.ScrollOffset); + Point locationTop = textView.PointToScreen(visualLocationTop - textView.ScrollOffset); + + // Let's use device dependent units for everything + Size completionWindowSize = new Size(this.ActualWidth, this.ActualHeight).TransformToDevice(textView); + Rect bounds = new Rect(location, completionWindowSize); + Rect workingScreen = System.Windows.Forms.Screen.GetWorkingArea(location.ToSystemDrawing()).ToWpf(); + if (!workingScreen.Contains(bounds)) { + if (bounds.Left < workingScreen.Left) { + bounds.X = workingScreen.Left; + } else if (bounds.Right > workingScreen.Right) { + bounds.X = workingScreen.Right - bounds.Width; + } + if (bounds.Bottom > workingScreen.Bottom) { + bounds.Y = locationTop.Y - bounds.Height; + IsUp = true; + } else { + IsUp = false; + } + if (bounds.Y < workingScreen.Top) { + bounds.Y = workingScreen.Top; + } + } + // Convert the window bounds to device independent units + bounds = bounds.TransformFromDevice(textView); + this.Left = bounds.X; + this.Top = bounds.Y; + + Debug.WriteLine($"Position: {Left}x{Top}"); + } + + /// + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + if (sizeInfo.HeightChanged && IsUp) { + this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height; + } + } + + /// + /// Gets/sets whether the completion window should expect text insertion at the start offset, + /// which not go into the completion region, but before it. + /// + /// This property allows only a single insertion, it is reset to false + /// when that insertion has occurred. + public bool ExpectInsertionBeforeStart { get; set; } + + void textArea_Document_Changing(object sender, DocumentChangeEventArgs e) + { + if (e.Offset + e.RemovalLength == this.StartOffset && e.RemovalLength > 0) { + HideCompletion(); // removal immediately in front of completion segment: close the window + // this is necessary when pressing backspace after dot-completion + } + if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) { + StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion); + this.ExpectInsertionBeforeStart = false; + } else { + StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion); + } + EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/ICompletionData.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/ICompletionData.cs new file mode 100644 index 000000000..9f16876b3 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/ICompletionData.cs @@ -0,0 +1,49 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// Describes an entry in the . + /// + public interface ICompletionData + { + /// + /// Gets the image. + /// + BitmapSource Image { get; } + + /// + /// Gets the text. This property is used to filter the list of visible elements. + /// + string Text { get; } + + /// + /// Gets the description. + /// + object Description { get; set; } + + /// + /// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items + /// which the user is accessing most frequently. + /// + double Priority { get; set; } + + /// + /// Perform the completion. + /// + /// The text area on which completion is performed. + /// The text segment that was used by the completion window if + /// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset). + /// The EventArgs used for the insertion request. + /// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how + /// the insertion was triggered. + void Complete(ScriptEditor editor); + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/IOverloadProvider.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/IOverloadProvider.cs new file mode 100644 index 000000000..8aaaa8d41 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/IOverloadProvider.cs @@ -0,0 +1,42 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// Provides the items for the OverloadViewer. + /// + public interface IOverloadProvider : INotifyPropertyChanged + { + /// + /// Gets/Sets the selected index. + /// + int SelectedIndex { get; set; } + + /// + /// Gets the number of overloads. + /// + int Count { get; } + + /// + /// Gets the text 'SelectedIndex of Count'. + /// + string CurrentIndexText { get; } + + /// + /// Gets the current header. + /// + object CurrentHeader { get; } + + /// + /// Gets the current content. + /// + object CurrentContent { get; } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.cs new file mode 100644 index 000000000..bea06f3b3 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.cs @@ -0,0 +1,94 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Controls; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// A popup-like window that is attached to a text segment. + /// + public class InsightWindow : CompletionWindowBase + { + static InsightWindow() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(InsightWindow), + new FrameworkPropertyMetadata(typeof(InsightWindow))); + AllowsTransparencyProperty.OverrideMetadata(typeof(InsightWindow), + new FrameworkPropertyMetadata(Boxes.True)); + } + + /// + /// Creates a new InsightWindow. + /// + public InsightWindow(TextArea textArea) : base(textArea) + { + this.CloseAutomatically = true; + AttachEvents(); + } + + /// + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + + Rect caret = this.TextArea.Caret.CalculateCaretRectangle(); + Point pointOnScreen = this.TextArea.TextView.PointToScreen(caret.Location - this.TextArea.TextView.ScrollOffset); + Rect workingArea = System.Windows.Forms.Screen.FromPoint(pointOnScreen.ToSystemDrawing()).WorkingArea.ToWpf().TransformFromDevice(this); + + MaxHeight = workingArea.Height; + MaxWidth = Math.Min(workingArea.Width, Math.Max(1000, workingArea.Width * 0.6)); + } + + /// + /// Gets/Sets whether the insight window should close automatically. + /// The default value is true. + /// + public bool CloseAutomatically { get; set; } + + /// + protected override bool CloseOnFocusLost { + get { return this.CloseAutomatically; } + } + + void AttachEvents() + { + this.TextArea.Caret.PositionChanged += CaretPositionChanged; + } + + /// + protected override void DetachEvents() + { + this.TextArea.Caret.PositionChanged -= CaretPositionChanged; + base.DetachEvents(); + } + + void CaretPositionChanged(object sender, EventArgs e) + { + if (this.CloseAutomatically) { + int offset = this.TextArea.Caret.Offset; + if (offset < this.StartOffset || offset > this.EndOffset) { + Close(); + } + } + } + } + + /// + /// TemplateSelector for InsightWindow to replace plain string content by a TextBlock with TextWrapping. + /// + internal sealed class InsightWindowTemplateSelector : DataTemplateSelector + { + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is string) + return (DataTemplate)((FrameworkElement)container).FindResource("TextBlockTemplate"); + + return null; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.xaml b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.xaml new file mode 100644 index 000000000..6b5a67388 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/InsightWindow.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadInsightWindow.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadInsightWindow.cs new file mode 100644 index 000000000..5ed165aad --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadInsightWindow.cs @@ -0,0 +1,58 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Input; + +using Tango.Scripting.Editors.Editing; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// Insight window that shows an OverloadViewer. + /// + public class OverloadInsightWindow : InsightWindow + { + OverloadViewer overloadViewer = new OverloadViewer(); + + /// + /// Creates a new OverloadInsightWindow. + /// + public OverloadInsightWindow(TextArea textArea) : base(textArea) + { + overloadViewer.Margin = new Thickness(2,0,0,0); + this.Content = overloadViewer; + } + + /// + /// Gets/Sets the item provider. + /// + public IOverloadProvider Provider { + get { return overloadViewer.Provider; } + set { overloadViewer.Provider = value; } + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled && this.Provider.Count > 1) { + switch (e.Key) { + case Key.Up: + e.Handled = true; + overloadViewer.ChangeIndex(-1); + break; + case Key.Down: + e.Handled = true; + overloadViewer.ChangeIndex(+1); + break; + } + if (e.Handled) { + UpdateLayout(); + UpdatePosition(); + } + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadViewer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadViewer.cs new file mode 100644 index 000000000..f70229a8d --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/CodeCompletion/OverloadViewer.cs @@ -0,0 +1,101 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; + +namespace Tango.Scripting.Editors.CodeCompletion +{ + /// + /// Represents a text between "Up" and "Down" buttons. + /// + public class OverloadViewer : Control + { + static OverloadViewer() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(OverloadViewer), + new FrameworkPropertyMetadata(typeof(OverloadViewer))); + } + + /// + /// The text property. + /// + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register("Text", typeof(string), typeof(OverloadViewer)); + + /// + /// Gets/Sets the text between the Up and Down buttons. + /// + public string Text { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Button upButton = (Button)this.Template.FindName("PART_UP", this); + upButton.Click += (sender, e) => { + e.Handled = true; + ChangeIndex(-1); + }; + + Button downButton = (Button)this.Template.FindName("PART_DOWN", this); + downButton.Click += (sender, e) => { + e.Handled = true; + ChangeIndex(+1); + }; + } + + /// + /// The ItemProvider property. + /// + public static readonly DependencyProperty ProviderProperty = + DependencyProperty.Register("Provider", typeof(IOverloadProvider), typeof(OverloadViewer)); + + /// + /// Gets/Sets the item provider. + /// + public IOverloadProvider Provider { + get { return (IOverloadProvider)GetValue(ProviderProperty); } + set { SetValue(ProviderProperty, value); } + } + + /// + /// Changes the selected index. + /// + /// The relative index change - usual values are +1 or -1. + public void ChangeIndex(int relativeIndexChange) + { + IOverloadProvider p = this.Provider; + if (p != null) { + int newIndex = p.SelectedIndex + relativeIndexChange; + if (newIndex < 0) + newIndex = p.Count - 1; + if (newIndex >= p.Count) + newIndex = 0; + p.SelectedIndex = newIndex; + } + } + } + + sealed class CollapseIfSingleOverloadConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return ((int)value < 2) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} -- cgit v1.3.1