AvalonEdit show selection over other formatting - c#

I'm currently using the following code as a LineTransformer with an AvalonEdit TextEditor. I want to be able to highlight the current single search result with a selection, however the selection is barely visible because the formatting of the DocumentColorizingTransformer has precedence over showing highlighted text. How do I get the highlighted selection to show instead of or before the formatting?
public class ColorizeSearchResults : DocumentColorizingTransformer {
public ColorizeSearchResults() : base() {
SearchTerm = "";
MatchCase = false;
}
public string SearchTerm { get; set; }
public bool MatchCase { get; set; }
protected override void ColorizeLine(DocumentLine line) {
if (SearchTerm.Length == 0)
return;
int lineStartOffset = line.Offset;
string text = CurrentContext.Document.GetText(line);
int count = 0;
int start = 0;
int index;
while ((index = text.IndexOf(SearchTerm, start, MatchCase ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase)) >= 0) {
base.ChangeLinePart(
lineStartOffset + index,
lineStartOffset + index + SearchTerm.Length,
(VisualLineElement element) => {
element.TextRunProperties.SetForegroundBrush(Brushes.White);
element.TextRunProperties.SetBackgroundBrush(Brushes.Magenta);
});
start = index + 1;
count++;
}
}
}
Example of formatting showing over selection

See http://avalonedit.net/documentation/html/c06e9832-9ef0-4d65-ac2e-11f7ce9c7774.htm for AvalonEdit render flow.
The selection layer is rendered before text. So if the text has background, it overrides the selection background. Fortunately we can set the background to Brush.Transparent (or a mix of the Selection.Brush and your own color).
Solution: I've modified the SelectionColorizer code to reset selection background to transparent:
class SelectionColorizerWithBackground : ColorizingTransformer
{
ICSharpCode.AvalonEdit.Editing.TextArea _textArea;
public SelectionColorizerWithBackground(
ICSharpCode.AvalonEdit.Editing.TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this._textArea = textArea;
}
protected override void Colorize(ITextRunConstructionContext context)
{
int lineStartOffset = context.VisualLine.FirstDocumentLine.Offset;
int lineEndOffset = context.VisualLine.LastDocumentLine.Offset +
context.VisualLine.LastDocumentLine.TotalLength;
foreach (var segment in _textArea.Selection.Segments)
{
int segmentStart = segment.StartOffset;
if (segmentStart >= lineEndOffset)
continue;
int segmentEnd = segment.EndOffset;
if (segmentEnd <= lineStartOffset)
continue;
int startColumn;
if (segmentStart < lineStartOffset)
startColumn = 0;
else
startColumn = context.VisualLine.ValidateVisualColumn(
segment.StartOffset, segment.StartVisualColumn,
_textArea.Selection.EnableVirtualSpace);
int endColumn;
if (segmentEnd > lineEndOffset)
endColumn =
_textArea.Selection.EnableVirtualSpace
? int.MaxValue
: context.VisualLine
.VisualLengthWithEndOfLineMarker;
else
endColumn = context.VisualLine.ValidateVisualColumn(
segment.EndOffset, segment.EndVisualColumn,
_textArea.Selection.EnableVirtualSpace);
ChangeVisualElements(
startColumn, endColumn,
element => {
element.TextRunProperties.SetBackgroundBrush(
System.Windows.Media.Brushes.Transparent);
if (_textArea.SelectionForeground != null)
{
element.TextRunProperties.SetForegroundBrush(
_textArea.SelectionForeground);
}
});
}
}
}
To use the code you are supposed to do the following:
var lineTransformers = textEditor.TextArea.TextView.LineTransformers;
// Remove the original SelectionColorizer.
// Note: if you have syntax highlighting you need to do something else
// to avoid clearing other colorizers. If too complicated you can skip
// this step but to suffer a 2x performance penalty.
lineTransformers.Clear();
lineTransformers.Add(new ColorizeSearchResults());
lineTransformers.Add(
new SelectionColorizerWithBackground(textEditor.TextArea));

After I've tried my solutions extensively, I'd like to add a few points:
While my other solution above appears to work, you'll have some subpixel artefacts when the rectangles are supposed to be tiled. If that is unacceptable you can implement an IBackgroundRenderer. (That happens to be my chosen solution.) If you want some code you may request here, but I doubt whether it will be useful.
BTW, since your question is about search result, most likely you can use https://github.com/icsharpcode/AvalonEdit/blob/697ff0d38c95c9e5a536fbc05ae2307ec9ef2a63/ICSharpCode.AvalonEdit/Search/SearchResultBackgroundRenderer.cs unmodified (or modify it if you don't want the rounded borders).
You may use element.BackgroundBrush = Brushes.Magenta; instead of element.TextRunProperties.SetBackgroundBrush(Brushes.Magenta);. AvalonEdit appears to draw the background with a rectangle with 3px radius.
There is also a RichTextColorizer starting from AvalonEdit 5.01. I don't know how to use it though because it is not referenced in other files. And the (most likely unwanted) rounded rectangles in the previous paragraph are likely to be present.

So here's my final product based almost entirely off of the existing AvalonEdit SearchResultBackgroundRenderer.
This works a little different than my post's colorizer as you have to modify the search results manually instead of it doing it for you. But that may also save some computation time.
If your search doesn't use Regex, then you can easily modify SearchResult to instead just pass in a start offset and length for the constructor.
/// <summary>A search result storing a match and text segment.</summary>
public class SearchResult : TextSegment {
/// <summary>The regex match for the search result.</summary>
public Match Match { get; }
/// <summary>Constructs the search result from the match.</summary>
public SearchResult(Match match) {
this.StartOffset = match.Index;
this.Length = match.Length;
this.Match = match;
}
}
/// <summary>Colorizes search results behind the selection.</summary>
public class ColorizeSearchResultsBackgroundRenderer : IBackgroundRenderer {
/// <summary>The search results to be modified.</summary>
TextSegmentCollection<SearchResult> currentResults = new TextSegmentCollection<SearchResult>();
/// <summary>Constructs the search result colorizer.</summary>
public ColorizeSearchResultsBackgroundRenderer() {
Background = new SolidColorBrush(Color.FromRgb(246, 185, 77));
Background.Freeze();
}
/// <summary>Gets the layer on which this background renderer should draw.</summary>
public KnownLayer Layer {
get {
// draw behind selection
return KnownLayer.Selection;
}
}
/// <summary>Causes the background renderer to draw.</summary>
public void Draw(TextView textView, DrawingContext drawingContext) {
if (textView == null)
throw new ArgumentNullException("textView");
if (drawingContext == null)
throw new ArgumentNullException("drawingContext");
if (currentResults == null || !textView.VisualLinesValid)
return;
var visualLines = textView.VisualLines;
if (visualLines.Count == 0)
return;
int viewStart = visualLines.First().FirstDocumentLine.Offset;
int viewEnd = visualLines.Last().LastDocumentLine.EndOffset;
foreach (SearchResult result in currentResults.FindOverlappingSegments(viewStart, viewEnd - viewStart)) {
BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder();
geoBuilder.AlignToWholePixels = true;
geoBuilder.BorderThickness = 0;
geoBuilder.CornerRadius = 0;
geoBuilder.AddSegment(textView, result);
Geometry geometry = geoBuilder.CreateGeometry();
if (geometry != null) {
drawingContext.DrawGeometry(Background, null, geometry);
}
}
}
/// <summary>Gets the search results for modification.</summary>
public TextSegmentCollection<SearchResult> CurrentResults {
get { return currentResults; }
}
/// <summary>Gets or sets the background brush for the search results.</summary>
public Brush Background { get; set; }
}
In order to make use of the background renderer:
var searchColorizor = new ColorizeSearchResultsBackgroundRenderer();
textEditor.TextArea.TextView.BackgroundRenderers.Add(searchColorizor);

Related

Updating a RichTextBox with many UI-access operations

I have a sub-classed RichTextBox that I use to display chat information scrubbed from a game. Each chat line is saved into a custom object that stores the text as well as some meta-data. The RTB then uses this meta data to do some formatting. Here's the relevant code section:
public class ChatLine
{ //stub-class with code irrelevant to the question removed
public string Text { get; set; };
public Color Color { get; set; };
public DateTime Timestamp { get; set; };
public string Name { get; set; }; //name of the player where this text originated
}
public partial class CustomRichTextbox : RichTextBox
{
private readonly Object syncRoot = new();
private readonly List<ChatLine> displayedChatLines = new();
internal IReadOnlyCollection<ChatLine> DisplayedChatLines
{
get
{
return displayedChatLines.AsReadOnly();
}
}
public CustomRichTextbox()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
}
/// <summary>
/// Appends text to the current text of the textbox, ends the line, and returns the first line position that was appended.
/// </summary>
/// <remarks>
/// The second parameter should be supplied if you want to enable name display per line, and supplying the name you don't want displayed.
/// </remarks>
internal int AppendLines(IEnumerable<ChatLine> lines, string name = "")
{
//when the RTB is empty line count will be 0, which is what we should return
//when the RTB is not empty then we need to return line count - 1
//the Math.Max call ensures we take care of both cases
var pos = Math.Max(0, Lines.Length - 1);
lock (syncRoot)
{
base.AppendText(String.Join(String.Empty, lines.Select(FormatLine)));
displayedChatLines.AddRange(lines);
}
//paint the lines after we append (THIS LOOP IS TIME CONSUMING)
for (int i = lines.Count() - 1; i >= 0; i--)
{
var index = pos + i;
var line = displayedChatLines[index];
var start = this.GetFirstCharIndexFromLine(index);
var timestampEnd = this.Find(">", start, RichTextBoxFinds.None);
var textStart = timestampEnd + 2;
var lineLength = this.Lines[index].Length;
//color in timestamp
this.Select(start, timestampEnd - start + 1);
this.SelectionColor = this.ForeColor;
this.SelectionBackColor = this.BackColor;
this.SelectionFont = this.Font;
this.SelectionAlignment = HorizontalAlignment.Left;
if (String.IsNullOrEmpty(name) || name == line.Name)
{ //line doesn't contain name
this.Select(textStart, start + lineLength - textStart + 1);
this.SelectionColor = line.Color;
this.SelectionBackColor = (line.Color == Color.White) ? Color.DarkGray : this.BackColor;
}
else
{ //line contains name
var nameStart = this.Find($" {line.Name}", start, start + lineLength, RichTextBoxFinds.Reverse);
this.Select(textStart, nameStart - textStart);
this.SelectionColor = line.Color;
this.SelectionBackColor = (line.Color == Color.White) ? Color.DarkGray : this.BackColor;
this.Select(nameStart, start + lineLength - nameStart + 1);
this.SelectionColor = Color.DarkGray;
this.SelectionBackColor = this.BackColor;
this.SelectionFont = new Font(this.Font.FontFamily, 8);
this.SelectionAlignment = HorizontalAlignment.Right; //this actually causes the entire line to be right aligned
}
}
this.DeselectAll(); //necessary otherwise restoring scroll position won't work
return pos;
string FormatLine(ChatLine line)
{
return (String.IsNullOrEmpty(name) || name == line.Name) ?
$"<{line.Timestamp: hh:mm:ss tt}> {line.Text}{Environment.NewLine}" :
$"<{line.Timestamp: hh:mm:ss tt}> {line.Text} {line.Name}{Environment.NewLine}";
}
}
}
This works fine...for updates that have few lines. When it needs to update more (say 1000) the UI thread chokes up for a few seconds to do all the painting. I've identified the time-consuming code (noted in the code above) because when I comment that loop out then there's no problem.
Normally I'd just move the time-consuming code to a backgroundworker or use an async/await pattern, but the problem is all the code that's in the time consuming section requires UI-thread access! What do I do when I can't push the time-consuming stuff to another thread?

make highlight with pdfIum pdfSearchManager

I need to programmatically search pdf with pdfSearchManager and highlight the text. The code can make the search but with no highlight.
Plese see my code and correct me.
PdfSearchManager a = new PdfSearchManager(pdfRenderer1);
a.Reset();
a.MatchWholeWord = true;
a.HighlightAllMatches = true;
MessageBox.Show(a.Search(textBox1.Text).ToString());
As we can see on GitHub (if that is the right PdfSearchManager you're using)
Search() is a boolean so its only return true or false so it won't show you any text with .ToString()
Search() :
/// <summary>
/// Searches for the specified text.
/// </summary>
/// <param name="text">The text to search.</param>
/// <returns>Whether any matches were found.</returns>
It returns: Whether any matches were found
and not : Matches that were found
If you're trying to highlight the matches and not display them as I thought at the beginning then you should try using a.UpdateHighlights() ( I never used PdfSearchManager before but it might work)
From PdfSearchManager source code :
public bool Search(string text)
{
Renderer.Markers.Clear();
if (String.IsNullOrEmpty(text))
{
_matches = null;
_bounds = null;
}
else
{
_matches = Renderer.Document.Search(text, MatchCase, MatchWholeWord);
_bounds = GetAllBounds();
}
_offset = -1;
UpdateHighlights();
return _matches != null && _matches.Items.Count > 0;
}
there is private method UpdateHighlights and AddMatch :
private void UpdateHighlights()
{
Renderer.Markers.Clear();
if (_matches == null)
return;
if (_highlightAllMatches)
{
for (int i = 0; i < _matches.Items.Count; i++)
{
AddMatch(i, i == _offset);
}
}
else if (_offset != -1)
{
AddMatch(_offset, true);
}
}
private void AddMatch(int index, bool current)
{
foreach (var pdfBounds in _bounds[index])
{
var bounds = new RectangleF(
pdfBounds.Bounds.Left - 1,
pdfBounds.Bounds.Top + 1,
pdfBounds.Bounds.Width + 2,
pdfBounds.Bounds.Height - 2
);
var marker = new PdfMarker(
pdfBounds.Page,
bounds,
current ? CurrentMatchColor : MatchColor,
current ? CurrentMatchBorderColor : MatchBorderColor,
current ? CurrentMatchBorderWidth : MatchBorderWidth
);
Renderer.Markers.Add(marker);
}
}
I think the Search method had add all the marker from searched text.
Now, how to make it visible in pdfiumViewer.
Turn out that i am referenced to wrong renderer.
my previous code was :
PdfSearchManager a = new PdfSearchManager(pdfRenderer1);
where pdfRenderer1 is the name of component which i put on the Form
The code should be :
PdfSearchManager a = new PdfSearchManager(pdfViewer1.Renderer);
The highlight searched text is run as expected.

Highlighting words in RichEditBox

It is needed to highlight by fiven colour a substring in the document RichEditBox. For this purpose I wrote a method:
private async Task ChangeTextColor(string text, Color color)
{
string textStr;
bool theEnd = false;
int startTextPos = 0;
myRichEdit.Document.GetText(TextGetOptions.None, out textStr);
while (theEnd == false)
{
myRichEdit.Document.GetRange(startTextPos, textStr.Length).GetText(TextGetOptions.None, out textStr);
var isFinded = myRichEdit.Document.GetRange(startTextPos, textStr.Length).FindText(text, textStr.Length, FindOptions.None);
if (isFinded != 0)
{
string textStr2;
textStr2 = myRichEdit.Document.Selection.Text;
var dialog = new MessageDialog(textStr2);
await dialog.ShowAsync();
myRichEdit.Document.Selection.CharacterFormat.BackgroundColor = color;
startTextPos = myRichEdit.Document.Selection.EndPosition;
myRichEdit.Document.ApplyDisplayUpdates();
}
else
{
theEnd = true;
}
}
}
In the debugger you can see that there is a substring and isFinded is equal with the number of signs (or symbols) in the found substring. It means the fragment is found and judging by the description of the method FindText should be highlighted but it isn't. In textStr2 an empty line returns and, correspondingly, the colour doesn't change. I cannot identify the reasons of the error.
The code you postted did not set the selection, so the myRichEdit.Document.Selection is null. You can use ITextRange.SetRange to set the selection. And you can use ITextRange.FindText method to find the string in the selection.
For example:
private void ChangeTextColor(string text, Color color)
{
string textStr;
myRichEdit.Document.GetText(TextGetOptions.None, out textStr);
var myRichEditLength = textStr.Length;
myRichEdit.Document.Selection.SetRange(0, myRichEditLength);
int i = 1;
while (i > 0)
{
i = myRichEdit.Document.Selection.FindText(text, myRichEditLength, FindOptions.Case);
ITextSelection selectedText = myRichEdit.Document.Selection;
if (selectedText != null)
{
selectedText.CharacterFormat.BackgroundColor = color;
}
}
}

how to get specific colored words in richtextbox using c#.net

I want to select all the words within richtextbox which has blue color. How can I do this?
In richtextbox, there are some keywords of blue color. I want to get all these keywords as a collection.
By "select", I assume you mean "find". I don't believe you can actually select multiple discontinuous ranges of text in a RichTextBox.
Assuming my understanding is correct, here's some moderately-tested code I've been working on recently. Let me know how it works for you.
Be aware that all the textual content in a RichTextBox is actually stored in a FlowDocument, accessed via the Document property. To iterate through the strings you need to walk hierarchy of TextElement classes in theFlowDocument. The following does so, returning each string and a stack representing the hierarchy, possibly transformed by a selector method:
public static IEnumerable<KeyValuePair<Stack<T>, string>> WalkTextElements<T>(FlowDocument doc, Func<DependencyObject, Stack<T>, T> selector)
{
// Inspiration: http://www.bryanewert.net/journal/2010/5/26/how-to-explore-the-contents-of-a-flowdocument.html
if (doc != null)
{
var stack = new Stack<T>();
// Start with a TextPointer to FlowDocument.ContentStart
TextPointer t = doc.ContentStart;
// Keep a TextPointer for FlowDocument.ContentEnd handy, so we know when we're done.
TextPointer e = doc.ContentEnd;
// Keep going until the TextPointer is equal to or greater than ContentEnd.
while ((t != null) && (t.CompareTo(e) < 0))
{
// Identify the type of content immediately adjacent to the text pointer.
TextPointerContext context = t.GetPointerContext(LogicalDirection.Forward);
// ElementStart is an "opening tag" which defines the structure of the document, e.g. a paragraph declaration.
if (context == TextPointerContext.ElementStart)
{
stack.Push(selector(t.Parent, stack));
}
// An EmbeddedElement, e.g. a UIContainer.
else if (context == TextPointerContext.EmbeddedElement)
{
; // Do nothing.
}
// The document's text content.
else if (context == TextPointerContext.Text)
{
stack.Push(selector(t.Parent, stack));
yield return new KeyValuePair<Stack<T>, string>(stack, t.GetTextInRun(LogicalDirection.Forward));
stack.Pop();
}
// ElementEnd is a "closing tag".
else if (context == TextPointerContext.ElementEnd)
{
stack.Pop();
}
else
{
throw new System.Exception("Unhandled TextPointerContext " + context.ToString());
}
// Advance to the next ContentElement in the FlowDocument.
t = t.GetNextContextPosition(LogicalDirection.Forward);
}
}
}
With this, we can enumerate the strings with background color explicitly overridden:
/// <summary>
/// Enumerate all the strings in a given flow document that are have an explicit background color.
/// </summary>
/// <param name="doc"></param>
/// <param name="includeFlowDocumentColor">true to consider overrides on the entire FlowDocument itself, else false.</param>
/// <returns></returns>
public static IEnumerable<KeyValuePair<Brush, string>> WalkBackgroundColoredTexts(FlowDocument doc, bool includeFlowDocumentColor)
{
foreach (var pair in WalkTextElements<Brush>(doc, (d, s) => SelectTextBackgroundBrush(d, s, includeFlowDocumentColor)))
{
var brush = pair.Key.Peek();
if (brush != null)
{
yield return new KeyValuePair<Brush, string>(brush, pair.Value);
}
}
}
static Brush SelectTextBackgroundBrush(DependencyObject element, Stack<Brush> brushes, bool includeFlowDocumentColor)
{
//http://blogs.msdn.com/b/prajakta/archive/2006/10/11/flowdocument-content-model.aspx
//http://msdn.microsoft.com/en-us/library/aa970786%28v=vs.110%29.aspx
var textElement = element as TextElement;
if (textElement != null)
{
var brush = textElement.Background;
if (brush != null)
return brush;
return PeekOrDefault(brushes);
}
var tableColumn = element as TableColumn;
if (tableColumn != null)
{
var brush = tableColumn.Background;
if (brush != null)
return brush;
return PeekOrDefault(brushes);
}
if (includeFlowDocumentColor)
{
var doc = element as FlowDocument;
if (doc != null)
{
var brush = doc.Background;
if (brush != null)
return brush;
return PeekOrDefault(brushes);
}
}
return null;
}
static T PeekOrDefault<T>(Stack<T> stack)
{
return (stack.Count == 0 ? default(T) : stack.Peek());
}
You probably want to ignore background colors set on the flow document itself and get only specific text runs with background color set, which is why I added the argument.
Given the strings, you may still need to tokenize them into words.

Only top row of DataGridView updating?

I have a DataGridView that I'm populating from a list. The function that edits this list is called LoadCollectionData()'. Extra rows get added to the list just fine, and the relevant data pertaining to that row populates when the row is added.
The problem is that later on when other data is being changed that'd alter what's displayed on the datagrid, only the top row continues to update, all of the others remain the same.
Here's the code for the method:
public bool haschanged = false;
public class KeywordDensity
{
public bool included { get; set; }
public string keyword { get; set; }
public string occurences { get; set; }
public string density { get; set; }
}
public int WordCount(string txtToCount)
{
string pattern = "\\w+";
Regex regex = new Regex(pattern);
int CountedWords = regex.Matches(txtToCount).Count;
return CountedWords;
}
public int KeywordCount(string txtToCount, string pattern)
{
Regex regex = new Regex(pattern);
int CountedWords = regex.Matches(txtToCount).Count;
return CountedWords;
}
public List<KeywordDensity> LoadCollectionData()
{
string thearticle = txtArticle.Text.ToLower();
string keywordslower = txtKeywords.Text.ToLower();
string[] keywordsarray = keywordslower.Split('\r');
List<KeywordDensity> lsikeywords = new List<KeywordDensity>();
bool isincluded = false;
double keywordcount = 0;
double wordcount = WordCount(thearticle);
double thedensity = 0;
foreach (string s in keywordsarray)
{
if (s != "")
{
keywordcount = KeywordCount(thearticle, s);
thedensity = keywordcount / wordcount;
thedensity = Math.Round(thedensity, 4) * 100;
if (thearticle.Contains(s))
{
isincluded = true;
}
else
{
isincluded = false;
}
lsikeywords.Add(new KeywordDensity()
{
included = isincluded,
keyword = s,
occurences = keywordcount.ToString(),
density = thedensity.ToString() + "%"
});
}
}
return lsikeywords;
}
private void txtArticle_TextChanged(object sender, EventArgs e)
{
if (haschanged == false)
haschanged = true;
lblWordCountNum.Text = WordCount(txtArticle.Text).ToString();
dataGrid.DataSource = LoadCollectionData();
}
private void dataGrid_MouseUp(object sender, MouseEventArgs e)
{
int cursorpos = 0;
string copied = "";
if (dataGrid.CurrentCellAddress.X == 1) //Only grab content if the "Keyword" column has been clicked on
copied = " " + dataGrid.CurrentCell.Value.ToString() + " ";
cursorpos = txtArticle.SelectionStart;
txtArticle.Text = txtArticle.Text.Insert(cursorpos, copied);
}
What's even more odd, is that when I click on any of the rows, then they immediately update. However, unless the row is clicked on (unless it's the top one) it doesn't update.
Because of this, I suspect there may be some property I need to set on the dataGrid itself, or I need to somehow tell each row to refresh through code.
What's the dealio?
EDIT: It appears that the only reason that the cell that's clicked on updates is because I actively grab content from the cell. I commented out the code below and it stopped updating even when clicked on. It then would only update the top row's values and that's it.
Code:
//Moved above in EDIT 3
EDIT 2: Here's the class declaration for KeywordDensity:
//Moved above in EDIT 3
EDIT 3: Posted whole schebang.
I modified the code slightly, try this code.
string[] keywordsarray = keywordslower.Split
(new char[] {'\r','\n' }, StringSplitOptions.RemoveEmptyEntries);
You may need to Invalidate() the control to trigger a repaint.
call the DataBind() method of the datagrid. That should do.
Update
There's a ResetBindings() in that case.

Categories

Resources