// 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 search functionality for AvalonEdit. It is displayed in the top-right corner of the TextArea. /// public class SearchPanel : Control { TextArea textArea; SearchInputHandler handler; TextDocument currentDocument; SearchResultBackgroundRenderer renderer; TextBox searchTextBox; SearchPanelAdorner adorner; #region DependencyProperties /// /// Dependency property for . /// public static readonly DependencyProperty UseRegexProperty = DependencyProperty.Register("UseRegex", typeof(bool), typeof(SearchPanel), new FrameworkPropertyMetadata(false, SearchPatternChangedCallback)); /// /// Gets/sets whether the search pattern should be interpreted as regular expression. /// public bool UseRegex { get { return (bool)GetValue(UseRegexProperty); } set { SetValue(UseRegexProperty, value); } } /// /// Dependency property for . /// public static readonly DependencyProperty MatchCaseProperty = DependencyProperty.Register("MatchCase", typeof(bool), typeof(SearchPanel), new FrameworkPropertyMetadata(false, SearchPatternChangedCallback)); /// /// Gets/sets whether the search pattern should be interpreted case-sensitive. /// public bool MatchCase { get { return (bool)GetValue(MatchCaseProperty); } set { SetValue(MatchCaseProperty, value); } } /// /// Dependency property for . /// public static readonly DependencyProperty WholeWordsProperty = DependencyProperty.Register("WholeWords", typeof(bool), typeof(SearchPanel), new FrameworkPropertyMetadata(false, SearchPatternChangedCallback)); /// /// Gets/sets whether the search pattern should only match whole words. /// public bool WholeWords { get { return (bool)GetValue(WholeWordsProperty); } set { SetValue(WholeWordsProperty, value); } } /// /// Dependency property for . /// public static readonly DependencyProperty SearchPatternProperty = DependencyProperty.Register("SearchPattern", typeof(string), typeof(SearchPanel), new FrameworkPropertyMetadata("", SearchPatternChangedCallback)); /// /// 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(SearchPanel), 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(SearchPanel), new FrameworkPropertyMetadata(new Localization())); /// /// Gets/sets the localization for the SearchPanel. /// public Localization Localization { get { return (Localization)GetValue(LocalizationProperty); } set { SetValue(LocalizationProperty, value); } } #endregion static void MarkerBrushChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { SearchPanel panel = d as SearchPanel; if (panel != null) { panel.renderer.MarkerBrush = (Brush)e.NewValue; } } static SearchPanel() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchPanel), new FrameworkPropertyMetadata(typeof(SearchPanel))); } ISearchStrategy strategy; static void SearchPatternChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { SearchPanel panel = d as SearchPanel; if (panel != null) { panel.ValidateSearchText(); panel.UpdateSearch(); } } void UpdateSearch() { // only reset as long as there are results // if no results are found, the "no matches found" message should not flicker. // if results are found by the next run, the message will be hidden inside DoSearch ... if (renderer.CurrentResults.Any()) messageView.IsOpen = false; strategy = SearchStrategyFactory.Create(SearchPattern ?? "", !MatchCase, WholeWords, UseRegex ? SearchMode.RegEx : SearchMode.Normal); OnSearchOptionsChanged(new SearchOptionsChangedEventArgs(SearchPattern, MatchCase, UseRegex, WholeWords)); DoSearch(true); } /// /// Creates a new SearchPanel. /// [Obsolete("Use the Install method instead")] public SearchPanel() { } /// /// Attaches this SearchPanel 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 SearchPanel and installs it to the TextEditor's TextArea. /// /// This is a convenience wrapper. public static SearchPanel Install(TextEditor editor) { if (editor == null) throw new ArgumentNullException("editor"); return Install(editor.TextArea); } /// /// Creates a SearchPanel and installs it to the TextArea. /// public static SearchPanel Install(TextArea textArea) { if (textArea == null) throw new ArgumentNullException("textArea"); #pragma warning disable 618 SearchPanel panel = new SearchPanel(); panel.AttachInternal(textArea); panel.handler = new SearchInputHandler(textArea, panel); textArea.DefaultInputHandler.NestedInputHandlers.Add(panel.handler); return panel; } /// /// Removes the SearchPanel from the TextArea. /// public void Uninstall() { CloseAndRemove(); textArea.DefaultInputHandler.NestedInputHandlers.Remove(handler); } void AttachInternal(TextArea textArea) { this.textArea = textArea; adorner = new SearchPanelAdorner(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.FindNext, (sender, e) => FindNext())); this.CommandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, (sender, e) => FindPrevious())); this.CommandBindings.Add(new CommandBinding(SearchCommands.CloseSearchPanel, (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; } void ValidateSearchText() { if (searchTextBox == null) return; var be = searchTextBox.GetBindingExpression(TextBox.TextProperty); try { Validation.ClearInvalid(be); UpdateSearch(); } catch (SearchPatternException ex) { var ve = new ValidationError(be.ParentBinding.ValidationRules[0], be, ex.Message, ex); Validation.MarkInvalid(be, ve); } } /// /// Reactivates the SearchPanel 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 next occurrence in the file. /// public void FindNext() { SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + 1); if (result == null) result = renderer.CurrentResults.FirstSegment; if (result != null) { SelectResult(result); } } /// /// Moves to the previous occurrence in the file. /// public void FindPrevious() { SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset); if (result != null) result = renderer.CurrentResults.GetPreviousSegment(result); if (result == null) result = renderer.CurrentResults.LastSegment; if (result != null) { SelectResult(result); } } ToolTip messageView = new ToolTip { Placement = PlacementMode.Bottom, StaysOpen = false }; void DoSearch(bool changeSelection) { if (IsClosed) return; renderer.CurrentResults.Clear(); if (!string.IsNullOrEmpty(SearchPattern)) { int offset = textArea.Caret.Offset; if (changeSelection) { textArea.ClearSelection(); } // We cast from ISearchResult to SearchResult; this is safe because we always use the built-in strategy foreach (SearchResult result in strategy.FindAll(textArea.Document, 0, textArea.Document.TextLength)) { if (changeSelection && result.StartOffset >= offset) { SelectResult(result); changeSelection = false; } renderer.CurrentResults.Add(result); } if (!renderer.CurrentResults.Any()) { messageView.IsOpen = true; messageView.Content = Localization.NoMatchesFoundText; messageView.PlacementTarget = searchTextBox; } else messageView.IsOpen = false; } textArea.TextView.InvalidateLayer(KnownLayer.Selection); } 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; messageView.IsOpen = false; if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) FindPrevious(); else FindNext(); 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 SearchPanel. /// public void Close() { bool hasFocus = this.IsKeyboardFocusWithin; var layer = AdornerLayer.GetAdornerLayer(textArea); if (layer != null) layer.Remove(adorner); 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 SearchPanel 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 search 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 SearchOptions are changed inside the SearchPanel. /// public event EventHandler SearchOptionsChanged; /// /// Raises the event. /// protected virtual void OnSearchOptionsChanged(SearchOptionsChangedEventArgs e) { if (SearchOptionsChanged != null) { SearchOptionsChanged(this, e); } } } /// /// EventArgs for event. /// public class SearchOptionsChangedEventArgs : 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 SearchOptionsChangedEventArgs instance. /// public SearchOptionsChangedEventArgs(string searchPattern, bool matchCase, bool useRegex, bool wholeWords) { this.SearchPattern = searchPattern; this.MatchCase = matchCase; this.UseRegex = useRegex; this.WholeWords = wholeWords; } } class SearchPanelAdorner : Adorner { SearchPanel panel; public SearchPanelAdorner(TextArea textArea, SearchPanel 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); } } }