Integrating our FindWindow class

In order to integrate our FindWindow class with our TextArea, we will need to add some functionality to one of them. We can choose to keep the logic inside either class. For this example, I will put all of the searching logic into the TextArea instead of the FindWindow, but it could easily be kept in either one.

Bring back up your textarea.py file ready for editing. We are going to begin with one more import statement at the top:

import tkinter.messagebox as msg

We will be using the messagebox module to convey information to the user regarding the results of their searches. We have met this module already in Chapter 1, Meet Tkinter.

When the user enters text to be searched for in the find area, we will highlight it as yellow within our TextArea widget and scroll the view down to it. In order to do that, we will again take advantage of the tagging abilities of our Text widget.

As we know, a tag does nothing until we configure it. Within our __init__ method, we shall configure a tag and keep a reference to some variables as class attributes:

self.tag_configure('find_match', background="yellow")
self.find_match_index = None
self.find_search_starting_index = 1.0

We create a tag called find_match and tell our widget to color the background of any range tagged by this in yellow.

The find_match_index attribute will hold a reference to the last beginning index discovered by our search method, and the find_search_starting_index will hold an index at which we want to begin each subsequent search.

If you look back into your FindWindow class, you will see that it calls a method called find on its master. Let's implement this method in our TextArea class now:

def find(self, text_to_find):
length = tk.IntVar()
idx = self.search(text_to_find, self.find_search_starting_index,
stopindex=tk.END, count=length)

if idx:
self.tag_remove('find_match', 1.0, tk.END)

end = f'{idx}+{length.get()}c'
self.tag_add('find_match', idx, end)
self.see(idx)

self.find_search_starting_index = end
self.find_match_index = idx

The method will take one argument—a string of text to search for.

Much like before, we create an IntVar to hold the length of a match and find the first occurrence with the search method.

Instead of the normal while loop, we instead use an if statement to handle the tagging. All matches we find are given our find_match tag and the range at which to match is calculated using the +nc capabilities we saw in our Highlighter.

Once the tag is added, we can use a method called see on our TextArea in order to scroll the match into view. This method takes an index as its argument and will scroll the Text widget until that index is viewable to the user. We pass the index of the beginning of our match to this method so that our user will instantly see their matches.

Now that the tagging and scrolling has been completed, we need to update our attributes which keep track of where we need to begin and end searching. This is because we don't want to find all matches in one go—we must wait for the user to press the find button once again before we do the next search attempt.

Once the search reaches the end of the widget and finds no more matches, we will inform the user using a message box:

else:
if self.find_match_index != 1.0:
if msg.askyesno("No more results", "No further matches.
Repeat from the beginning?"
):
self.find_search_starting_index = 1.0
self.find_match_index = None
return self.find(text_to_find)
else:
msg.showinfo("No Matches", "No matching text found")

The message box will display different information depending on whether or not any matches were found. In order to detect this, we will call upon our find_match_index variable, since this kept a reference to the beginning index of a match. If this is still the initial value of 1.0, then we know that we did not find any matches.

If this attribute is a value other than 1.0, then a match occurred somewhere within the Text widget. We will ask the user if they wish to repeat the search from the beginning. This question is asked via an askyesno box.

Should the user wish to repeat the search, we need to reset the attributes, keeping track of search indexes to their default values and call the find method once again. Otherwise, we do not need to perform any action.

If no matches were found during the search, we will simply display this information to the user with a showinfo box.

That completes all of the logic necessary for the find button. Now, onto the replace button. Once again, checking our findwindow.py file, we can see that this calls a method called replace_text and passes it the contents of both Entry widgets. Let's create this functionality in our TextArea class:

def replace_text(self, target, replacement):
if self.find_match_index:
end = f"{self.find_match_index}+{len(target)}c"
self.replace(self.find_match_index, end, replacement)

When replacing text, we must make sure that we have first found a match. If we have, then the value of our find_match_index will be set. We check for this before performing any logic.

Supposing we have a match, we need to get the range of its indexes, much like if we were adding a tag to it. Instead of adding a tag, however, we will be replacing its content with the replacement string.

We calculate the indexes of our needed range using the same method as always. Instead of having an IntVar with the length of the match, we instead need to call the len function on it. We add the length of the string to the find_match_index to calculate the end of the needed range.

The replace method of a Text widget will erase the content between the range of the first two arguments and insert the replacement text, given as the third argument, in its place. We can use this to easily replace a match with the provided replacement text.

Once a replacement has taken place, we need to adjust where the next call to the find method will begin its search, since the content of the Text widget has changed. A suitable place to resume searching from is the beginning of the line at which the replacement was made:

self.find_search_starting_index = f"{self.find_match_index} linestart"
self.find_match_index = None

To get this index, we just have to add the word linestart to the end of the index of our replacement. This value is then set as our find_search_starting_index attribute, to be used by our find method.

To prevent another attempt at replacement, we set our find_match_index back to None to avoid any more calls to replace_text from happening until another match is found.

The final thing to implement for our FindWindow is a method which will remove the find_match tags when the user closes the find/replace window. We will call this cancel_find:

def cancel_find(self):
self.find_search_starting_index = 1.0
self.find_match_index = None
self.tag_remove('find_match', 1.0, tk.END)

All that needs to be done is resetting the attributes back to their default values and using the tag_remove method to remove all tags called find_match throughout the whole of the widget.

We will now need to update our FindWindow class to make it call this method. Open up findwindow.py and make the following changes:

self.cancel_button = ttk.Button(bottom_frame, text="Cancel", command=self.on_cancel)

def
on_cancel(self):
self.master.cancel_find()
self.destroy()

In the __init__ method, we need to change the command attribute of the cancel_button from self.destroy to self.on_cancel.

The on_cancel method will call cancel_find on the Text widget before calling self.destroy as before.

Like with the others, we now need to add this class into our TextEditor in order to make it usable.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset