I've got a find next and previous function and edited it so that when the user selects text in a textbox and clicks on either Find Next or Find Previous button, the find feature will start it's index from the selected character and go through each search result (initially the feature wasn't there). To get the starting index of the selected text I created a function:
private int GetIntialCharPos(string Text)
{
int row = Variables._TextBox.GetLineIndexFromCharacterIndex(Variables._TextBox.CaretIndex);
int col = Variables._TextBox.CaretIndex - Variables._TextBox.GetCharacterIndexFromLineIndex(row);
return col;
}
The function which does the Find Next and Previous goes as follows:
private List<int> _matches;
private string _textToFind;
private bool _matchCase;
private int _matchIndex;
private void MoveToNextMatch(string textToFind, bool matchCase, bool forward)
{
if (_matches == null || _textToFind != textToFind || _matchCase != matchCase)
{
int startIndex = 0, matchIndex;
StringComparison mode = matchCase ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase;
_matches = new List<int>();
while (startIndex < Variables._TextBox.Text.Length && (matchIndex = Variables._TextBox.Text.IndexOf(textToFind, startIndex, mode)) >= 0)
{
_matches.Add(matchIndex);
startIndex = matchIndex + textToFind.Length;
}
_textToFind = textToFind;
_matchCase = matchCase;
_matchIndex = forward ? _matches.IndexOf(GetIntialCharPos(textToFind)) : _matches.IndexOf(GetIntialCharPos(textToFind)) - 1;
}
else
{
_matchIndex += forward ? 1 : -1;
if (_matchIndex < 0)
{
_matchIndex = _matches.Count - 1;
}
else if (_matchIndex >= _matches.Count)
{
_matchIndex = 0;
}
}
if (_matches.Count > 0)
{
Variables._TextBox.SelectionStart = _matches[_matchIndex];
Variables._TextBox.SelectionLength = textToFind.Length;
Variables._TextBox.Focus();
}
}
My issue is that once the user has selected the text he needs to search, and goes through the find next and previous buttons, and then he decides to select the text from a different index, rather than continuing the search from the selected index, it will maintain the default initial order which it goes by rather than starting from the selected index and going through each result from that. I created a small gif video here so you can take a better look at this problem.
How do I preserve the selected word index so every time the user selects from a different index it can start the search from the index in which the user selected rather than always starting from the start.
private int _matchIndex;
That's your problem variable. It retains the last match index but you don't know when the user changes it by himself. The TextBox class does not have a SelectionChanged event to tell you about it so there is no simple way to reset the variable.
Simply use a RichTextBox instead, it does have that event.
But it is much easier, this bug occurred because you added state unnecessarily by splitting of the searching operation into a separate class. State is in general a bad thing, it is a bug generator. You can trivially make the entire operation stateless by using the TextBox object directly:
private static void MoveToNextMatch(TextBoxBase box, bool forward) {
var needle = box.SelectedText;
var haystack = box.Text;
int index = box.SelectionStart;
if (forward) {
index = haystack.IndexOf(needle, index + 1);
if (index < 0) index = haystack.IndexOf(needle, 0);
}
else {
if (index == 0) index = -1;
else index = haystack.LastIndexOf(needle, index - 1);
if (index < 0) index = haystack.LastIndexOf(needle, haystack.Length - 1);
}
if (index >= 0) {
box.SelectionStart = index;
box.SelectionLength = needle.Length;
}
box.Focus();
}
Use the Last/IndexOf() method that takes a StringComparison to implement the matchCase argument.
Related
I'm using ScintillaNET in VisualStudio/C#.
When the user clicks (LMB or RMB) a specific word inside the text, I need to get the surrounding symbols. For example:
This is <a test> to show my <problem>
In this case, if the user clicks over the word "test", I want to retrieve the entire block between "<" and ">", so I need to get <a test>.
If the user clicks over "problem" I need to get <problem>.
I know that I can get the caret position then "navigate" (for loop) before the position (going left) to find the first occurence of "<", then "navigate" after the caret position (going right) to find the first occurrence of ">".
But is there any other better way to achieve this? Does Scintilla supply some methods to find them?
Thank you for your help!
I used this code to find a workaround, but frankly speaking I wish to find a better solution:
const char CHAR_START_BLOCK = '[';
const char CHAR_END_BLOCK = ']';
if(e.Button == MouseButtons.Right) {
int leftPos = -1;
int rightPos = -1;
//
// Search for CHAR_START_BLOCK
//
for(int i = scintilla1.CurrentPosition; i >= 1; i--) {
if(scintilla1.GetCharAt(i) == CHAR_START_BLOCK) {
leftPos = i;
break;
}
if(scintilla1.GetCharAt(i) == '\r') {
break;
}
if( (scintilla1.GetCharAt(i) == CHAR_END_BLOCK) && (i != scintilla1.CurrentPosition)) {
break;
}
}
if(leftPos != -1) {
//
// Search for CHAR_END_BLOCK
//
string currentLine = scintilla1.Lines[scintilla1.CurrentLine].Text;
for(int i = scintilla1.CurrentPosition; i <= (scintilla1.CurrentPosition + currentLine.len()); i++) {
if(scintilla1.GetCharAt(i) == CHAR_END_BLOCK) {
rightPos = i;
break;
}
}
LogManager.addLog("LEFT/RIGHT: " + scintilla1.GetTextRange(leftPos, (rightPos + 1 - leftPos)));
}
}
I have a .NET UWP TextBox with a good deal of text, and I want to search for a word in it. When I click on the button to start my search, it will find the first occurrence of this word. When I click again, it will find the second, like ctrl+f in Notepad).
I want to get focus on the found world, but when is text is long enough that there is a scrollbar in, it will not bring the found word into view.
This is a screengrab of the screen in this state, showing how I must resize the window to see the found word.
Here is my code for searching (textarea is of type TextBox):
private void Find(string text)
{
textarea.Focus(FocusState.Programmatic);
var start = textarea.SelectionStart + textarea.SelectionLength;
var found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1)
{
textarea.SelectionStart = 0;
found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1) return;
}
textarea.SelectionStart = found;
textarea.SelectionLength = text.Length;
}
I have already tried to put textarea.Focus(FocusState.Programmatic); at the end of method as well as textarea.Focus(FocusState.Pointer);, but neither helped.
UPDATE:
I've found that it's focusing correctly, but to the last found word (to position, where is the cursor before find next word), not to the currently found word.
So I need to update focus to current SelectionStart, not to the last. Any ideas? I have already tried to change SelectionStart again, replace text and update layout - nothing helps.
What you can do is to measure the height of your text until the index, and resize the textbox accordingly.
private static float GetTextHeightUntilIndex(TextBox textBox, int index)
{
var height = 0;
var textBuffer = textBox.Text;
// Remove everything after `index` in order to measure its size
textBox.Text = textBuffer.Substring(0, index);
textBox.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
var height = textBox.DesiredSize().Height;
// Put the full text back
textBox.Text = textBuffer;
return height;
}
private void Find(string text)
{
textarea.Focus(FocusState.Programmatic);
var start = textarea.SelectionStart + textarea.SelectionLength;
var found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1)
{
textarea.SelectionStart = 0;
found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1) return;
}
textarea.SelectionStart = found;
textarea.SelectionLength = text.Length;
// -------------------
var cursorPosInPx = GetTextHeightUntilIndex(textarea, found);
// First method: resize your textbox to the selected word
textarea.Height = cursorPosInPx;
// Second method: scroll the textbox
var grid = (Grid)VisualTreeHelper.GetChild(textarea, 0);
for (var i = 0; i <= VisualTreeHelper.GetChildrenCount(grid) - 1; i++)
{
object obj = VisualTreeHelper.GetChild(grid, i);
if (obj is ScrollViewer)
((ScrollViewer)obj).ChangeView(null, cursorPosInPx, null, true);
}
}
Be careful however, for the first method, depending on whatlayout your textbox is, resizing the control may have an unwanted effect or no effect at all.
I want to get the text between ';' character. However if there are 3 matches like that, i want to get the text between ';' character where current cursor position. I am using multine textbox.
Example;
select * from database;
----> **Lets say my cursor is here**
select
orders from
customer;
select * from employees;
So, i only want to get 'select orders from customer' text.
Could you please share your thoughts on it?
To achieve this, you first have to find all indicies of ;. To do this, iterate through all indicies (source):
private List<int> AllIndicesOf(string strToSearch, string fullText)
{
List<int> foundIndices = new List<int>();
for (int i = fullText.IndexOf(strToSearch); i > -1; i = fullText.IndexOf(strToSearch, i + 1))
{
foundIndices.Add(i + 1);
}
return foundIndices;
}
Then you have to compare your position to those indices, since you only want the index (of ;) that follows immediately after your cursor:
List<int> indicies = AllIndicesOf(";", txtBxText.Text);
try
{
if (indicies.Count > 0)
{
int cursorPos = txtBxText.SelectionStart;
var indicesBefore = indicies.Where(x => x < cursorPos);
int beginIndex = indicesBefore.Count() > 0 ? indicesBefore.Last() : 0;
int endIndex = indicies.Where(x => x > beginIndex).First();
txtBxSelected.Text = txtBxText.Text.Substring(beginIndex, endIndex - beginIndex);
}
}
catch { }
The try-catch statement is used to prevent an Exception if your cursors position is after all other indices.
A sample project can be downloaded here.
This solution perfectly works although you need to check it again and consider some possible exceptions. I did not consider them myself because I thought it was better to be handled by you. I also used richTextBox which is better than the multi line text box. Enjoy the code bro
private void button1_Click(object sender, EventArgs e)
{
var ultimateResult = string.Empty;
var cusrPosition = richTextBox1.SelectionStart;
var currentStr = string.Empty;
var strDic = new Dictionary<string,int>();
var textArr = richTextBox1.Text.ToCharArray();
for (var i = 0; i < textArr.Count(); i++)
{
if (textArr[i] != ';')
currentStr = currentStr + textArr[i];
else
{
strDic.Add(currentStr,i);
currentStr = string.Empty;
}
}
ultimateResult = strDic.First(item => item.Value >= cusrPosition).Key;
textBox1.Text = ultimateResult;
}
I have a RichTextBox with -by example- this text:
"This is my Text"
Now I want to "search" the RichTextBox for a Text (String), by example:
"Text"
Now "Text" should be selected/highlighted (for each one) in the RichTextBox..
There is something like:
myRichTextBox.Select();
but here I have to set a StartPosition and so on, but I want to search for String!
How could I do this? (Searched stackoverflow, didn't find something similiar..)
int start = 0;
int indexOfSearchText = 0;
private void btnFind_Click(object sender, EventArgs e)
{
int startindex = 0;
if(txtSearch.Text.Length > 0)
startindex = FindMyText(txtSearch.Text.Trim(), start, rtb.Text.Length);
// If string was found in the RichTextBox, highlight it
if (startindex >= 0)
{
// Set the highlight color as red
rtb.SelectionColor = Color.Red;
// Find the end index. End Index = number of characters in textbox
int endindex = txtSearch.Text.Length;
// Highlight the search string
rtb.Select(startindex, endindex);
// mark the start position after the position of
// last search string
start = startindex + endindex;
}
}
public int FindMyText(string txtToSearch, int searchStart, int searchEnd)
{
// Unselect the previously searched string
if (searchStart > 0 && searchEnd > 0 && indexOfSearchText >= 0)
{
rtb.Undo();
}
// Set the return value to -1 by default.
int retVal = -1;
// A valid starting index should be specified.
// if indexOfSearchText = -1, the end of search
if (searchStart >= 0 && indexOfSearchText >=0)
{
// A valid ending index
if (searchEnd > searchStart || searchEnd == -1)
{
// Find the position of search string in RichTextBox
indexOfSearchText = rtb.Find(txtToSearch, searchStart, searchEnd, RichTextBoxFinds.None);
// Determine whether the text was found in richTextBox1.
if (indexOfSearchText != -1)
{
// Return the index to the specified search text.
retVal = indexOfSearchText;
}
}
}
return retVal;
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
start = 0;
indexOfSearchText = 0;
}
CheckOut this article if you dont understand this code...
http://www.dotnetcurry.com/ShowArticle.aspx?ID=146
You can only have one selection in a text box. What you want is to highlight the found text.
You could achieve it like this:
Find the positions of the text you want to highlight using repeated calls to myRichTextBox.Text.IndexOf with the last found index + 1 as the start position.
Highlight the found texts using the default RichTextBox capabilities.
You can use the Find method to find the startindex of your searched text:
int indexToText = myRichTextBox.Find(searchText);
You can then use this index and the Select method to select the text:
int endIndex = searchText.Length;
myRichTextBox.Select(indexToText , endIndex);
private void Txt_Search_Box_TT_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
FOFO:
int start =
RtfAll_Messages.Find(Txt_Search_Box_TT.Text, RtfAll_Messages.SelectionStart + 1,
RichTextBoxFinds.None);
if (start >= 0)
RtfAll_Messages.Select(start, Txt_Search_Box_TT.Text.Length);
else
{
start = 0;
RtfAll_Messages.SelectionStart = 0;
RtfAll_Messages.SelectionLength = 0;
}
}
}
I have a Navigation-bar in my program that allows you to navigate the different sections in my TextBox, but the problem I have is that this doesn't work if the Text I am scrolling to is already visible on the screen.
Like in this example, if I try to jump from Section 1 to Section 3, it won't work as it's already visible.
But, in this example if I jump to Section 3, it works fine as it's not already visible.
The scrolling function I use is very simple:
if (nLine > 0 && nLine <= textBox.LineCount)
textBox.ScrollToLine(nLine - 1);
I hope that someone can shed some light on an alternative solution that allows me to scroll even if the text is already visible.
Edit: Added solution.
This is a code snippet from my project.
private static void ScrollToLineCallback(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
var textBox = (TextBox)target;
int newLineValue;
if (Int32.TryParse(e.NewValue.ToString(), out newLineValue))
{
if (newLineValue > 0 && newLineValue <= textBox.LineCount) // Validate
{
textBox.ScrollToLine(newLineValue - 1); // Scroll to Line
// Check and see if we are at the line we want.
if (textBox.GetFirstVisibleLineIndex() <= newLineValue && textBox.GetLastVisibleLineIndex() >= newLineValue)
{
// If not lets move to the desired location
int newLineCorrectionValue = newLineValue - textBox.GetFirstVisibleLineIndex() - 2; // How much further do we need to scroll down?
for (int i = 0; i < newLineCorrectionValue; i++)
{
textBox.LineDown(); // Scroll down
}
}
}
}
}
You could use GetCharacterIndexFromLineIndex to get the index of the beginning of the desired line and then set the CaretIndex to that value.
Because I don't really know, what you are trying to achieve, another possibility is to use LineUp and LineDown in conjunction with GetFirstVisibleLineIndex and GetLastVisibleLineIndex.
private void TextBoxGotoLine(
TextBox textbox1,
int linenum)
{
var target_cpos = textbox1.GetCharacterIndexFromLineIndex(linenum);
var target_char_rect = textbox1.GetRectFromCharacterIndex(target_cpos);
var first_char_rect = textbox1.GetRectFromCharacterIndex(0);
textbox1.ScrollToVerticalOffset(
target_char_rect.Top -
first_char_rect.Top
);
}
I found out if Wrapping is enabled its more complications:
private void TextBoxGotoLine(TextBox textbox1, int linenum)
{
// int Linenum is the Absolute Line, not including
// effect of Textbox Wrapping.
if (textbox1.TextWrapping == TextWrapping.Wrap)
{
// If textbox Wrapping is on, we need to
// Correct the Linenum for Wrapping which adds extra lines
int cidx = 0;
bool found = false;
int ln = 0;
char[] tmp = textbox1.Text.ToCharArray();
for (cidx = 0; cidx < tmp.Length; cidx++)
{
if (tmp[cidx] == '\n')
{
ln++;
}
if (ln == linenum)
{
found = true;
break;
}
}
if (!found)
return;
linenum = textbox1.GetLineIndexFromCharacterIndex(cidx+1);
}
// Non-Wrapping TextBox
var target_cpos = textbox1.GetCharacterIndexFromLineIndex(linenum);
var target_char_rect = textbox1.GetRectFromCharacterIndex(target_cpos);
var first_char_rect = textbox1.GetRectFromCharacterIndex(0);
textbox1.ScrollToVerticalOffset(target_char_rect.Top - first_char_rect.Top);
}