Free cookie consent management tool by TermsFeed Policy Generator

source: stable/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Search/SearchPanel.cs @ 17328

Last change on this file since 17328 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 16.3 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.Linq;
22using System.Text;
23using System.Text.RegularExpressions;
24using System.Windows;
25using System.Windows.Controls;
26using System.Windows.Controls.Primitives;
27using System.Windows.Data;
28using System.Windows.Documents;
29using System.Windows.Input;
30using System.Windows.Media;
31using System.Windows.Threading;
32using ICSharpCode.AvalonEdit.Document;
33using ICSharpCode.AvalonEdit.Editing;
34using ICSharpCode.AvalonEdit.Folding;
35using ICSharpCode.AvalonEdit.Rendering;
36
37namespace ICSharpCode.AvalonEdit.Search
38{
39  /// <summary>
40  /// Provides search functionality for AvalonEdit. It is displayed in the top-right corner of the TextArea.
41  /// </summary>
42  public class SearchPanel : Control
43  {
44    TextArea textArea;
45    SearchInputHandler handler;
46    TextDocument currentDocument;
47    SearchResultBackgroundRenderer renderer;
48    TextBox searchTextBox;
49    SearchPanelAdorner adorner;
50   
51    #region DependencyProperties
52    /// <summary>
53    /// Dependency property for <see cref="UseRegex"/>.
54    /// </summary>
55    public static readonly DependencyProperty UseRegexProperty =
56      DependencyProperty.Register("UseRegex", typeof(bool), typeof(SearchPanel),
57                                  new FrameworkPropertyMetadata(false, SearchPatternChangedCallback));
58   
59    /// <summary>
60    /// Gets/sets whether the search pattern should be interpreted as regular expression.
61    /// </summary>
62    public bool UseRegex {
63      get { return (bool)GetValue(UseRegexProperty); }
64      set { SetValue(UseRegexProperty, value); }
65    }
66   
67    /// <summary>
68    /// Dependency property for <see cref="MatchCase"/>.
69    /// </summary>
70    public static readonly DependencyProperty MatchCaseProperty =
71      DependencyProperty.Register("MatchCase", typeof(bool), typeof(SearchPanel),
72                                  new FrameworkPropertyMetadata(false, SearchPatternChangedCallback));
73   
74    /// <summary>
75    /// Gets/sets whether the search pattern should be interpreted case-sensitive.
76    /// </summary>
77    public bool MatchCase {
78      get { return (bool)GetValue(MatchCaseProperty); }
79      set { SetValue(MatchCaseProperty, value); }
80    }
81   
82    /// <summary>
83    /// Dependency property for <see cref="WholeWords"/>.
84    /// </summary>
85    public static readonly DependencyProperty WholeWordsProperty =
86      DependencyProperty.Register("WholeWords", typeof(bool), typeof(SearchPanel),
87                                  new FrameworkPropertyMetadata(false, SearchPatternChangedCallback));
88   
89    /// <summary>
90    /// Gets/sets whether the search pattern should only match whole words.
91    /// </summary>
92    public bool WholeWords {
93      get { return (bool)GetValue(WholeWordsProperty); }
94      set { SetValue(WholeWordsProperty, value); }
95    }
96   
97    /// <summary>
98    /// Dependency property for <see cref="SearchPattern"/>.
99    /// </summary>
100    public static readonly DependencyProperty SearchPatternProperty =
101      DependencyProperty.Register("SearchPattern", typeof(string), typeof(SearchPanel),
102                                  new FrameworkPropertyMetadata("", SearchPatternChangedCallback));
103   
104    /// <summary>
105    /// Gets/sets the search pattern.
106    /// </summary>
107    public string SearchPattern {
108      get { return (string)GetValue(SearchPatternProperty); }
109      set { SetValue(SearchPatternProperty, value); }
110    }
111   
112    /// <summary>
113    /// Dependency property for <see cref="MarkerBrush"/>.
114    /// </summary>
115    public static readonly DependencyProperty MarkerBrushProperty =
116      DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(SearchPanel),
117                                  new FrameworkPropertyMetadata(Brushes.LightGreen, MarkerBrushChangedCallback));
118   
119    /// <summary>
120    /// Gets/sets the Brush used for marking search results in the TextView.
121    /// </summary>
122    public Brush MarkerBrush {
123      get { return (Brush)GetValue(MarkerBrushProperty); }
124      set { SetValue(MarkerBrushProperty, value); }
125    }
126   
127    /// <summary>
128    /// Dependency property for <see cref="Localization"/>.
129    /// </summary>
130    public static readonly DependencyProperty LocalizationProperty =
131      DependencyProperty.Register("Localization", typeof(Localization), typeof(SearchPanel),
132                                  new FrameworkPropertyMetadata(new Localization()));
133   
134    /// <summary>
135    /// Gets/sets the localization for the SearchPanel.
136    /// </summary>
137    public Localization Localization {
138      get { return (Localization)GetValue(LocalizationProperty); }
139      set { SetValue(LocalizationProperty, value); }
140    }
141    #endregion
142   
143    static void MarkerBrushChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
144    {
145      SearchPanel panel = d as SearchPanel;
146      if (panel != null) {
147        panel.renderer.MarkerBrush = (Brush)e.NewValue;
148      }
149    }
150   
151    static SearchPanel()
152    {
153      DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchPanel), new FrameworkPropertyMetadata(typeof(SearchPanel)));
154    }
155   
156    ISearchStrategy strategy;
157   
158    static void SearchPatternChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
159    {
160      SearchPanel panel = d as SearchPanel;
161      if (panel != null) {
162        panel.ValidateSearchText();
163        panel.UpdateSearch();
164      }
165    }
166
167    void UpdateSearch()
168    {
169      // only reset as long as there are results
170      // if no results are found, the "no matches found" message should not flicker.
171      // if results are found by the next run, the message will be hidden inside DoSearch ...
172      if (renderer.CurrentResults.Any())
173        messageView.IsOpen = false;
174      strategy = SearchStrategyFactory.Create(SearchPattern ?? "", !MatchCase, WholeWords, UseRegex ? SearchMode.RegEx : SearchMode.Normal);
175      OnSearchOptionsChanged(new SearchOptionsChangedEventArgs(SearchPattern, MatchCase, UseRegex, WholeWords));
176      DoSearch(true);
177    }
178   
179    /// <summary>
180    /// Creates a new SearchPanel.
181    /// </summary>
182    [Obsolete("Use the Install method instead")]
183    public SearchPanel()
184    {
185    }
186   
187    /// <summary>
188    /// Attaches this SearchPanel to a TextArea instance.
189    /// </summary>
190    [Obsolete("Use the Install method instead")]
191    public void Attach(TextArea textArea)
192    {
193      if (textArea == null)
194        throw new ArgumentNullException("textArea");
195      AttachInternal(textArea);
196    }
197   
198    /// <summary>
199    /// Creates a SearchPanel and installs it to the TextEditor's TextArea.
200    /// </summary>
201    /// <remarks>This is a convenience wrapper.</remarks>
202    public static SearchPanel Install(TextEditor editor)
203    {
204      if (editor == null)
205        throw new ArgumentNullException("editor");
206      return Install(editor.TextArea);
207    }
208   
209    /// <summary>
210    /// Creates a SearchPanel and installs it to the TextArea.
211    /// </summary>
212    public static SearchPanel Install(TextArea textArea)
213    {
214      if (textArea == null)
215        throw new ArgumentNullException("textArea");
216      #pragma warning disable 618
217      SearchPanel panel = new SearchPanel();
218      panel.AttachInternal(textArea);
219      panel.handler = new SearchInputHandler(textArea, panel);
220      textArea.DefaultInputHandler.NestedInputHandlers.Add(panel.handler);
221      return panel;
222    }
223   
224    /// <summary>
225    /// Removes the SearchPanel from the TextArea.
226    /// </summary>
227    public void Uninstall()
228    {
229      CloseAndRemove();
230      textArea.DefaultInputHandler.NestedInputHandlers.Remove(handler);
231    }
232   
233    void AttachInternal(TextArea textArea)
234    {
235      this.textArea = textArea;
236      adorner = new SearchPanelAdorner(textArea, this);
237      DataContext = this;
238     
239      renderer = new SearchResultBackgroundRenderer();
240      currentDocument = textArea.Document;
241      if (currentDocument != null)
242        currentDocument.TextChanged += textArea_Document_TextChanged;
243      textArea.DocumentChanged += textArea_DocumentChanged;
244      KeyDown += SearchLayerKeyDown;
245     
246      this.CommandBindings.Add(new CommandBinding(SearchCommands.FindNext, (sender, e) => FindNext()));
247      this.CommandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, (sender, e) => FindPrevious()));
248      this.CommandBindings.Add(new CommandBinding(SearchCommands.CloseSearchPanel, (sender, e) => Close()));
249      IsClosed = true;
250    }
251
252    void textArea_DocumentChanged(object sender, EventArgs e)
253    {
254      if (currentDocument != null)
255        currentDocument.TextChanged -= textArea_Document_TextChanged;
256      currentDocument = textArea.Document;
257      if (currentDocument != null) {
258        currentDocument.TextChanged += textArea_Document_TextChanged;
259        DoSearch(false);
260      }
261    }
262
263    void textArea_Document_TextChanged(object sender, EventArgs e)
264    {
265      DoSearch(false);
266    }
267   
268    /// <inheritdoc/>
269    public override void OnApplyTemplate()
270    {
271      base.OnApplyTemplate();
272      searchTextBox = Template.FindName("PART_searchTextBox", this) as TextBox;
273    }
274   
275    void ValidateSearchText()
276    {
277      if (searchTextBox == null)
278        return;
279      var be = searchTextBox.GetBindingExpression(TextBox.TextProperty);
280      try {
281        Validation.ClearInvalid(be);
282        UpdateSearch();
283      } catch (SearchPatternException ex) {
284        var ve = new ValidationError(be.ParentBinding.ValidationRules[0], be, ex.Message, ex);
285        Validation.MarkInvalid(be, ve);
286      }
287    }
288   
289    /// <summary>
290    /// Reactivates the SearchPanel by setting the focus on the search box and selecting all text.
291    /// </summary>
292    public void Reactivate()
293    {
294      if (searchTextBox == null)
295        return;
296      searchTextBox.Focus();
297      searchTextBox.SelectAll();
298    }
299   
300    /// <summary>
301    /// Moves to the next occurrence in the file.
302    /// </summary>
303    public void FindNext()
304    {
305      SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + 1);
306      if (result == null)
307        result = renderer.CurrentResults.FirstSegment;
308      if (result != null) {
309        SelectResult(result);
310      }
311    }
312
313    /// <summary>
314    /// Moves to the previous occurrence in the file.
315    /// </summary>
316    public void FindPrevious()
317    {
318      SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset);
319      if (result != null)
320        result = renderer.CurrentResults.GetPreviousSegment(result);
321      if (result == null)
322        result = renderer.CurrentResults.LastSegment;
323      if (result != null) {
324        SelectResult(result);
325      }
326    }
327   
328    ToolTip messageView = new ToolTip { Placement = PlacementMode.Bottom, StaysOpen = false };
329
330    void DoSearch(bool changeSelection)
331    {
332      if (IsClosed)
333        return;
334      renderer.CurrentResults.Clear();
335     
336      if (!string.IsNullOrEmpty(SearchPattern)) {
337        int offset = textArea.Caret.Offset;
338        if (changeSelection) {
339          textArea.ClearSelection();
340        }
341        // We cast from ISearchResult to SearchResult; this is safe because we always use the built-in strategy
342        foreach (SearchResult result in strategy.FindAll(textArea.Document, 0, textArea.Document.TextLength)) {
343          if (changeSelection && result.StartOffset >= offset) {
344            SelectResult(result);
345            changeSelection = false;
346          }
347          renderer.CurrentResults.Add(result);
348        }
349        if (!renderer.CurrentResults.Any()) {
350          messageView.IsOpen = true;
351          messageView.Content = Localization.NoMatchesFoundText;
352          messageView.PlacementTarget = searchTextBox;
353        } else
354          messageView.IsOpen = false;
355      }
356      textArea.TextView.InvalidateLayer(KnownLayer.Selection);
357    }
358
359    void SelectResult(SearchResult result)
360    {
361      textArea.Caret.Offset = result.StartOffset;
362      textArea.Selection = Selection.Create(textArea, result.StartOffset, result.EndOffset);
363      textArea.Caret.BringCaretToView();
364      // show caret even if the editor does not have the Keyboard Focus
365      textArea.Caret.Show();
366    }
367   
368    void SearchLayerKeyDown(object sender, KeyEventArgs e)
369    {
370      switch (e.Key) {
371        case Key.Enter:
372          e.Handled = true;
373          messageView.IsOpen = false;
374          if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
375            FindPrevious();
376          else
377            FindNext();
378          if (searchTextBox != null) {
379            var error = Validation.GetErrors(searchTextBox).FirstOrDefault();
380            if (error != null) {
381              messageView.Content = Localization.ErrorText + " " + error.ErrorContent;
382              messageView.PlacementTarget = searchTextBox;
383              messageView.IsOpen = true;
384            }
385          }
386          break;
387        case Key.Escape:
388          e.Handled = true;
389          Close();
390          break;
391      }
392    }
393   
394    /// <summary>
395    /// Gets whether the Panel is already closed.
396    /// </summary>
397    public bool IsClosed { get; private set; }
398   
399    /// <summary>
400    /// Closes the SearchPanel.
401    /// </summary>
402    public void Close()
403    {
404      bool hasFocus = this.IsKeyboardFocusWithin;
405     
406      var layer = AdornerLayer.GetAdornerLayer(textArea);
407      if (layer != null)
408        layer.Remove(adorner);
409      messageView.IsOpen = false;
410      textArea.TextView.BackgroundRenderers.Remove(renderer);
411      if (hasFocus)
412        textArea.Focus();
413      IsClosed = true;
414     
415      // Clear existing search results so that the segments don't have to be maintained
416      renderer.CurrentResults.Clear();
417    }
418   
419    /// <summary>
420    /// Closes the SearchPanel and removes it.
421    /// </summary>
422    [Obsolete("Use the Uninstall method instead!")]
423    public void CloseAndRemove()
424    {
425      Close();
426      textArea.DocumentChanged -= textArea_DocumentChanged;
427      if (currentDocument != null)
428        currentDocument.TextChanged -= textArea_Document_TextChanged;
429    }
430   
431    /// <summary>
432    /// Opens the an existing search panel.
433    /// </summary>
434    public void Open()
435    {
436      if (!IsClosed) return;
437      var layer = AdornerLayer.GetAdornerLayer(textArea);
438      if (layer != null)
439        layer.Add(adorner);
440      textArea.TextView.BackgroundRenderers.Add(renderer);
441      IsClosed = false;
442      DoSearch(false);
443    }
444   
445    /// <summary>
446    /// Fired when SearchOptions are changed inside the SearchPanel.
447    /// </summary>
448    public event EventHandler<SearchOptionsChangedEventArgs> SearchOptionsChanged;
449   
450    /// <summary>
451    /// Raises the <see cref="SearchPanel.SearchOptionsChanged" /> event.
452    /// </summary>
453    protected virtual void OnSearchOptionsChanged(SearchOptionsChangedEventArgs e)
454    {
455      if (SearchOptionsChanged != null) {
456        SearchOptionsChanged(this, e);
457      }
458    }
459  }
460 
461  /// <summary>
462  /// EventArgs for <see cref="SearchPanel.SearchOptionsChanged"/> event.
463  /// </summary>
464  public class SearchOptionsChangedEventArgs : EventArgs
465  {
466    /// <summary>
467    /// Gets the search pattern.
468    /// </summary>
469    public string SearchPattern { get; private set; }
470   
471    /// <summary>
472    /// Gets whether the search pattern should be interpreted case-sensitive.
473    /// </summary>
474    public bool MatchCase { get; private set; }
475   
476    /// <summary>
477    /// Gets whether the search pattern should be interpreted as regular expression.
478    /// </summary>
479    public bool UseRegex { get; private set; }
480   
481    /// <summary>
482    /// Gets whether the search pattern should only match whole words.
483    /// </summary>
484    public bool WholeWords { get; private set; }
485   
486    /// <summary>
487    /// Creates a new SearchOptionsChangedEventArgs instance.
488    /// </summary>
489    public SearchOptionsChangedEventArgs(string searchPattern, bool matchCase, bool useRegex, bool wholeWords)
490    {
491      this.SearchPattern = searchPattern;
492      this.MatchCase = matchCase;
493      this.UseRegex = useRegex;
494      this.WholeWords = wholeWords;
495    }
496  }
497 
498  class SearchPanelAdorner : Adorner
499  {
500    SearchPanel panel;
501   
502    public SearchPanelAdorner(TextArea textArea, SearchPanel panel)
503      : base(textArea)
504    {
505      this.panel = panel;
506      AddVisualChild(panel);
507    }
508   
509    protected override int VisualChildrenCount {
510      get { return 1; }
511    }
512
513    protected override Visual GetVisualChild(int index)
514    {
515      if (index != 0)
516        throw new ArgumentOutOfRangeException();
517      return panel;
518    }
519   
520    protected override Size ArrangeOverride(Size finalSize)
521    {
522      panel.Arrange(new Rect(new Point(0, 0), finalSize));
523      return new Size(panel.ActualWidth, panel.ActualHeight);
524    }
525  }
526}
Note: See TracBrowser for help on using the repository browser.