diff --git a/ICSharpCode.AvalonEdit/GoToPanel.cs b/ICSharpCode.AvalonEdit/GoToPanel.cs new file mode 100644 index 00000000..51cc438a --- /dev/null +++ b/ICSharpCode.AvalonEdit/GoToPanel.cs @@ -0,0 +1,438 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Folding; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Search +{ + /// + /// Provides go to functionality for AvalonEdit. It is displayed in the top-right corner of the TextArea. + /// + public class GoToPanel : Control + { + TextArea textArea; + GoToInputHandler handler; + TextDocument currentDocument; + SearchResultBackgroundRenderer renderer; + TextBox searchTextBox; + Popup dropdownPopup; + GoToPanelAdorner adorner; + + #region + + /// + /// Dependency property for . + /// + public static readonly DependencyProperty SearchPatternProperty = + DependencyProperty.Register("SearchPattern", typeof(string), typeof(GoToPanel)); + + /// + /// Gets/sets the search pattern. + /// + public string SearchPattern { + get { return (string)GetValue(SearchPatternProperty); } + set { SetValue(SearchPatternProperty, value); } + } + + /// + /// Dependency property for . + /// + public static readonly DependencyProperty MarkerBrushProperty = + DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(GoToPanel), + new FrameworkPropertyMetadata(Brushes.LightGreen, MarkerBrushChangedCallback)); + + /// + /// Gets/sets the Brush used for marking search results in the TextView. + /// + public Brush MarkerBrush { + get { return (Brush)GetValue(MarkerBrushProperty); } + set { SetValue(MarkerBrushProperty, value); } + } + + /// + /// Dependency property for . + /// + public static readonly DependencyProperty LocalizationProperty = + DependencyProperty.Register("Localization", typeof(Localization), typeof(GoToPanel), + new FrameworkPropertyMetadata(new Localization())); + + /// + /// Gets/sets the localization for the GoToPanel. + /// + public Localization Localization { + get { return (Localization)GetValue(LocalizationProperty); } + set { SetValue(LocalizationProperty, value); } + } + #endregion + + static void MarkerBrushChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + GoToPanel panel = d as GoToPanel; + if (panel != null) { + panel.renderer.MarkerBrush = (Brush)e.NewValue; + } + } + + static GoToPanel() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(GoToPanel), new FrameworkPropertyMetadata(typeof(GoToPanel))); + } + + ISearchStrategy strategy; + + /// + /// Creates a new GoToPanel. + /// + GoToPanel() + { + } + + /// + /// Attaches this GoToPanel to a TextArea instance. + /// + [Obsolete("Use the Install method instead")] + public void Attach(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + AttachInternal(textArea); + } + + /// + /// Creates a GoToPanel and installs it to the TextEditor's TextArea. + /// + /// This is a convenience wrapper. + public static GoToPanel Install(TextEditor editor) + { + if (editor == null) + throw new ArgumentNullException("editor"); + return Install(editor.TextArea); + } + + /// + /// Creates a GoToPanel and installs it to the TextArea. + /// + public static GoToPanel Install(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + GoToPanel panel = new GoToPanel(); + panel.AttachInternal(textArea); + panel.handler = new GoToInputHandler(textArea, panel); + textArea.DefaultInputHandler.NestedInputHandlers.Add(panel.handler); + return panel; + } + + /// + /// Adds the commands used by GoToPanel to the given CommandBindingCollection. + /// + public void RegisterCommands(CommandBindingCollection commandBindings) + { + handler.RegisterGlobalCommands(commandBindings); + } + + /// + /// Removes the GoToPanel from the TextArea. + /// + public void Uninstall() + { + CloseAndRemove(); + textArea.DefaultInputHandler.NestedInputHandlers.Remove(handler); + } + + void AttachInternal(TextArea textArea) + { + this.textArea = textArea; + adorner = new GoToPanelAdorner(textArea, this); + DataContext = this; + + renderer = new SearchResultBackgroundRenderer(); + currentDocument = textArea.Document; + if (currentDocument != null) + currentDocument.TextChanged += textArea_Document_TextChanged; + textArea.DocumentChanged += textArea_DocumentChanged; + KeyDown += SearchLayerKeyDown; + + this.CommandBindings.Add(new CommandBinding(SearchCommands.GoTo, (sender, e) => GoToNext())); + this.CommandBindings.Add(new CommandBinding(SearchCommands.CloseGoToPanel, (sender, e) => Close())); + IsClosed = true; + } + + void textArea_DocumentChanged(object sender, EventArgs e) + { + if (currentDocument != null) + currentDocument.TextChanged -= textArea_Document_TextChanged; + currentDocument = textArea.Document; + if (currentDocument != null) { + currentDocument.TextChanged += textArea_Document_TextChanged; + //DoSearch(false); + } + } + + void textArea_Document_TextChanged(object sender, EventArgs e) + { + //DoSearch(false); + } + + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + searchTextBox = Template.FindName("PART_searchTextBox", this) as TextBox; + dropdownPopup = Template.FindName("PART_dropdownPopup", this) as Popup; + } + + + /// + /// Reactivates the GoToPanel by setting the focus on the search box and selecting all text. + /// + public void Reactivate() + { + if (searchTextBox == null) + return; + searchTextBox.Focus(); + searchTextBox.SelectAll(); + } + + /// + /// Moves to the found line in the file. + /// + public void GoToNext() + { + if (SearchPattern == "" || SearchPattern == null) + { + SearchPattern = "1"; + messageView.Content = Localization.Null; + return; + } + + else if (SearchPattern.All(char.IsDigit)) + { + int Line = Int32.Parse(SearchPattern); + + //messageView.Content = null; + + if (Line > textArea.Document.LineCount) + { + messageView.IsOpen = true; + messageView.Content = Localization.OverBounds; + messageView.PlacementTarget = searchTextBox; + Line = 1; + return; + } + + else if (SearchPattern.All(char.IsDigit)) + { + messageView.Content = null; + textArea.Caret.Offset = textArea.Document.GetOffset(Line, 0); + textArea.Caret.BringCaretToView(); + textArea.Caret.Show(); + } + + } + else + { + return; + } + } + + ToolTip messageView = new ToolTip { Placement = PlacementMode.Bottom, StaysOpen = true, Focusable = false }; + + void SelectResult(SearchResult result) + { + textArea.Caret.Offset = result.StartOffset; + textArea.Selection = Selection.Create(textArea, result.StartOffset, result.EndOffset); + textArea.Caret.BringCaretToView(); + // show caret even if the editor does not have the Keyboard Focus + textArea.Caret.Show(); + } + + void SearchLayerKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) { + case Key.Enter: + e.Handled = true; + GoToNext(); + if (searchTextBox != null) { + var error = Validation.GetErrors(searchTextBox).FirstOrDefault(); + if (error != null) { + //messageView.Content = Localization.ErrorText + " " + error.ErrorContent; + messageView.PlacementTarget = searchTextBox; + messageView.IsOpen = true; + } + } + break; + case Key.Escape: + e.Handled = true; + Close(); + break; + } + } + + /// + /// Gets whether the Panel is already closed. + /// + public bool IsClosed { get; private set; } + + /// + /// Closes the GoToPanel. + /// + public void Close() + { + bool hasFocus = this.IsKeyboardFocusWithin; + + var layer = AdornerLayer.GetAdornerLayer(textArea); + if (layer != null) + layer.Remove(adorner); + if (dropdownPopup != null) + dropdownPopup.IsOpen = false; + messageView.IsOpen = false; + textArea.TextView.BackgroundRenderers.Remove(renderer); + if (hasFocus) + textArea.Focus(); + IsClosed = true; + + // Clear existing search results so that the segments don't have to be maintained + renderer.CurrentResults.Clear(); + } + + /// + /// Closes the GoToPanel and removes it. + /// + [Obsolete("Use the Uninstall method instead!")] + public void CloseAndRemove() + { + Close(); + textArea.DocumentChanged -= textArea_DocumentChanged; + if (currentDocument != null) + currentDocument.TextChanged -= textArea_Document_TextChanged; + } + + /// + /// Opens the an existing GoTo panel. + /// + public void Open() + { + if (!IsClosed) return; + var layer = AdornerLayer.GetAdornerLayer(textArea); + if (layer != null) + layer.Add(adorner); + textArea.TextView.BackgroundRenderers.Add(renderer); + IsClosed = false; + //DoSearch(false); + } + + /// + /// Fired when GoToSearchOptions are changed inside the GoToPanel. + /// + public event EventHandler GoToSearchOptionsChanged; + + /// + /// Raises the event. + /// + protected virtual void GoToOnSearchOptionsChanged(GoToSearchOptionsChangedEventArgs e) + { + if (GoToSearchOptionsChanged != null) { + GoToSearchOptionsChanged(this, e); + } + } + } + + /// + /// Creats a new instance of GoToOptionsChangedEventArgs + /// + public class GoToSearchOptionsChangedEventArgs : EventArgs + { + /// + /// Gets the search pattern. + /// + public string SearchPattern { get; private set; } + + /// + /// Gets whether the search pattern should be interpreted case-sensitive. + /// + public bool MatchCase { get; private set; } + + /// + /// Gets whether the search pattern should be interpreted as regular expression. + /// + public bool UseRegex { get; private set; } + + /// + /// Gets whether the search pattern should only match whole words. + /// + public bool WholeWords { get; private set; } + + /// + /// Creates a new GoToSearchOptionsChangedEventArgs instance. + /// + public GoToSearchOptionsChangedEventArgs(string searchPattern, bool matchCase, bool useRegex, bool wholeWords) + { + this.SearchPattern = searchPattern; + this.MatchCase = matchCase; + this.UseRegex = useRegex; + this.WholeWords = wholeWords; + } + } + + class GoToPanelAdorner : Adorner + { + GoToPanel panel; + + public GoToPanelAdorner(TextArea textArea, GoToPanel panel) + : base(textArea) + { + this.panel = panel; + AddVisualChild(panel); + } + + protected override int VisualChildrenCount { + get { return 1; } + } + + protected override Visual GetVisualChild(int index) + { + if (index != 0) + throw new ArgumentOutOfRangeException(); + return panel; + } + + protected override Size ArrangeOverride(Size finalSize) + { + panel.Arrange(new Rect(new Point(0, 0), finalSize)); + return new Size(panel.ActualWidth, panel.ActualHeight); + } + } +} diff --git a/ICSharpCode.AvalonEdit/GoToPanel.xaml b/ICSharpCode.AvalonEdit/GoToPanel.xaml new file mode 100644 index 00000000..a3e60da6 --- /dev/null +++ b/ICSharpCode.AvalonEdit/GoToPanel.xaml @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/ICSharpCode.AvalonEdit/Localization.cs b/ICSharpCode.AvalonEdit/Localization.cs new file mode 100644 index 00000000..b6b46055 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Localization.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.ComponentModel; + +namespace ICSharpCode.AvalonEdit.Search +{ + /// + /// Holds default texts for buttons and labels in the SearchPanel. Override properties to add other languages. + /// + public class Localization + { + /// + /// Default: 'Match case' + /// + public virtual string MatchCaseText { + get { return "Match case"; } + } + + /// + /// Default: 'Match whole words' + /// + public virtual string MatchWholeWordsText { + get { return "Match whole words"; } + } + + + /// + /// Default: 'Use regular expressions' + /// + public virtual string UseRegexText { + get { return "Use regular expressions"; } + } + + /// + /// Default: 'Find next (F3)' + /// + public virtual string FindNextText { + get { return "Find next (F3)"; } + } + + /// + /// Default: 'Find previous (Shift+F3)' + /// + public virtual string FindPreviousText { + get { return "Find previous (Shift+F3)"; } + } + + /// + /// Default: 'Error: ' + /// + public virtual string ErrorText { + get { return "Error: "; } + } + + /// + /// Default: 'No matches found!' + /// + public virtual string NoMatchesFoundText { + get { return "No matches found!"; } + } + + /// + /// Default: 'No matches found!' + /// + public virtual string OverBounds + { + get { return "Over Bounds of Document!"; } + } + + /// + /// Default: 'No matches found!' + /// + public virtual string Null + { + get { return "Box is empty!"; } + } + } +} diff --git a/ICSharpCode.AvalonEdit/SearchCommands.cs b/ICSharpCode.AvalonEdit/SearchCommands.cs new file mode 100644 index 00000000..6d1c1c5c --- /dev/null +++ b/ICSharpCode.AvalonEdit/SearchCommands.cs @@ -0,0 +1,243 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Search +{ + /// + /// Search commands for AvalonEdit. + /// + public static class SearchCommands + { + /// + /// Finds the next occurrence in the file. + /// + public static readonly RoutedCommand FindNext = new RoutedCommand( + "FindNext", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.F3) } + ); + + /// + /// Finds the line in the file. + /// + public static readonly RoutedCommand GoTo = new RoutedCommand( + "GoTo", typeof(GoToPanel), + new InputGestureCollection { new KeyGesture(Key.G, ModifierKeys.Control) } + ); + + /// + /// Finds the previous occurrence in the file. + /// + public static readonly RoutedCommand FindPrevious = new RoutedCommand( + "FindPrevious", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.F3, ModifierKeys.Shift) } + ); + + /// + /// Closes the SearchPanel. + /// + public static readonly RoutedCommand CloseSearchPanel = new RoutedCommand( + "CloseSearchPanel", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.Escape) } + ); + + /// + /// Closes the SearchPanel. + /// + public static readonly RoutedCommand CloseGoToPanel = new RoutedCommand( + "CloseGoToPanel", typeof(GoToPanel), + new InputGestureCollection { new KeyGesture(Key.Escape) } + ); + } + + /// + /// TextAreaInputHandler that registers all search-related commands. + /// + public class SearchInputHandler : TextAreaInputHandler + { + /// + /// Creates a new SearchInputHandler and registers the search-related commands. + /// + [Obsolete("Use SearchPanel.Install instead")] + public SearchInputHandler(TextArea textArea) + : base(textArea) + { + RegisterCommands(this.CommandBindings); + panel = SearchPanel.Install(textArea); + } + + internal SearchInputHandler(TextArea textArea, SearchPanel panel) + : base(textArea) + { + RegisterCommands(this.CommandBindings); + this.panel = panel; + } + + internal void RegisterGlobalCommands(CommandBindingCollection commandBindings) + { + commandBindings.Add(new CommandBinding(ApplicationCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(SearchCommands.FindNext, ExecuteFindNext, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, ExecuteFindPrevious, CanExecuteWithOpenSearchPanel)); + } + + void RegisterCommands(ICollection commandBindings) + { + + commandBindings.Add(new CommandBinding(ApplicationCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(SearchCommands.FindNext, ExecuteFindNext, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, ExecuteFindPrevious, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.CloseSearchPanel, ExecuteCloseSearchPanel, CanExecuteWithOpenSearchPanel)); + } + + SearchPanel panel; + + void ExecuteFind(object sender, ExecutedRoutedEventArgs e) + { + panel.Open(); + if (!(TextArea.Selection.IsEmpty || TextArea.Selection.IsMultiline)) + panel.SearchPattern = TextArea.Selection.GetText(); + Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, (Action)delegate { panel.Reactivate(); }); + } + + void CanExecuteWithOpenSearchPanel(object sender, CanExecuteRoutedEventArgs e) + { + if (panel.IsClosed) { + e.CanExecute = false; + // Continue routing so that the key gesture can be consumed by another component. + e.ContinueRouting = true; + } else { + e.CanExecute = true; + e.Handled = true; + } + } + + void ExecuteFindNext(object sender, ExecutedRoutedEventArgs e) + { + if (!panel.IsClosed) { + panel.FindNext(); + e.Handled = true; + } + } + + void ExecuteFindPrevious(object sender, ExecutedRoutedEventArgs e) + { + if (!panel.IsClosed) { + panel.FindPrevious(); + e.Handled = true; + } + } + + void ExecuteCloseSearchPanel(object sender, ExecutedRoutedEventArgs e) + { + if (!panel.IsClosed) { + panel.Close(); + e.Handled = true; + } + } + + /// + /// Fired when SearchOptions are modified inside the SearchPanel. + /// + public event EventHandler SearchOptionsChanged { + add { panel.SearchOptionsChanged += value; } + remove { panel.SearchOptionsChanged -= value; } + } + } + + + + /// + /// TextAreaInputHandler that registers the goto line commands. + /// + public class GoToInputHandler : TextAreaInputHandler + { + /// + /// Creates a new SearchInputHandler and registers the search-related commands. + /// + [Obsolete("Use SearchPanel.Install instead")] + public GoToInputHandler(TextArea textArea) + : base(textArea) + { + RegisterCommands(this.CommandBindings); + panel2 = GoToPanel.Install(textArea); + } + + internal GoToInputHandler(TextArea textArea, GoToPanel panel2) + : base(textArea) + { + RegisterCommands(this.CommandBindings); + this.panel2 = panel2; + } + + internal void RegisterGlobalCommands(CommandBindingCollection commandBindings) + { + commandBindings.Add(new CommandBinding(AvalonEditCommands.GoTo, ExecuteGoTo)); + } + + void RegisterCommands(ICollection commandBindings) + { + commandBindings.Add(new CommandBinding(AvalonEditCommands.GoTo, ExecuteGoTo)); + commandBindings.Add(new CommandBinding(SearchCommands.CloseGoToPanel, ExecuteCloseSearchPanel, CanExecuteWithOpenSearchPanel)); + } + + GoToPanel panel2; + + void ExecuteGoTo(object sender, ExecutedRoutedEventArgs e) + { + panel2.Open(); + if (!(TextArea.Selection.IsEmpty || TextArea.Selection.IsMultiline)) + panel2.SearchPattern = TextArea.Selection.GetText(); + Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, (Action)delegate { panel2.Reactivate(); }); + } + + void CanExecuteWithOpenSearchPanel(object sender, CanExecuteRoutedEventArgs e) + { + if (panel2.IsClosed) + { + e.CanExecute = false; + // Continue routing so that the key gesture can be consumed by another component. + e.ContinueRouting = true; + } + else + { + e.CanExecute = true; + e.Handled = true; + } + } + + void ExecuteCloseSearchPanel(object sender, ExecutedRoutedEventArgs e) + { + if (!panel2.IsClosed) + { + panel2.Close(); + e.Handled = true; + } + } + } +}