Home > Blockchain >  Multiline insert selection tkinter Text widget
Multiline insert selection tkinter Text widget

Time:04-13

I would like to add a feature to my Text widget so that I can select multiple lines and comment it out. I currently have it working for single lines but am having trouble detecting the insert spanning multiple characters/lines. How do I get this kind of information. I know it is possible to detect it as you can remove multiple characters/lines at once with the Delete button. I want to comment out these chunks by adding a hashtag at the beginning of each line and it should also be able to remove multiline comments. How would I get the insert info to do such a thing?

Current commenting code:

#In __init__:
        self.text.bind("<Command-slash>", self.make_comment)
#Later...
    def make_comment(self, event):
        self_anchor = (str(self.text.index("insert")).split("."))[0]
        if self.text.get(self_anchor   ".0", self_anchor   ".1") != "#":
            self.text.insert(float("{}.0".format(self_anchor)), "#")
        else:
            self.text.delete(self_anchor   ".0", self_anchor   ".1")

CodePudding user response:

With the help of this answer by Bryan Oakley, I've solved this problem. I'll explain the code and how it works, but if you just want the working code, it'll be at the bottom of this answer.


In order to comment/uncomment all the lines in the user's selection, we need to know each line number. This can be done by using Python's range() function and the start and end line numbers of the user's selection.

To get the start line of the user's selection, we use this code:

first_line = self.text.index("sel.first").split(".")[0]

sel.first just means "the start index of the user's selection." Similarly, if we want to get the end line of the user's selection, we do the same thing but use sel.last:

last_line = self.text.index("sel.last").split(".")[0]

Then, we use the range() function to loop through both of those lines and each line in between them:

for line in range(first_line, last_line   1):
    # Comment or uncomment each line
    ...

Note we use last_line 1 to make sure we're including the last line, since Python's range() function stops before it gets to the second number.

Now, the only problem with this code is that if the user has not selected something, you get a _tkinter.TclError saying that text doesn't contain any characters tagged with "sel". So in order to be able to comment/uncomment a single line, you need to insert a try/except block:

try:
    first_line = self.text.index("sel.first").split(".")[0]
    last_line = self.text.index("sel.last").split(".")[0]
    for line in range(first_line, last_line   1):
        # Comment or uncomment each line
        ...
except tkinter.TclError:
    # Comment or uncomment a single line
    ...

The rest of the code looks very similar to what you already have; you check if the first character on the line is a #, if it is, then uncomment the line, if not, then comment the line.

There's just one more thing: at the end of self.make_comment(), you might need to add return "break"; on my system at least, the Ctrl / command also selects all the text. Returning "break" at the end of the function prevents it from doing that.


All that said, here is a complete reproducible example. You can comment/uncomment multiple lines at a time, or just one line, depending on how things are selected:

import tkinter

class Window(tkinter.Tk):
    """The main window."""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Our text widget
        self.text = tkinter.Text(self)
        self.text.insert("1.0", "Hello! This line should be commented!\n"*10)
        self.text.pack(expand=True, fill="both")
        
        # Bind the text widget
        self.text.bind("<Command-slash>", self.make_comment)
        
    def make_comment(self, event):
        
        # If this fails with an error, we know that the user only selected one line
        try:
            
            # Get the line number of the start and the end of the selection
            first_line = int(self.text.index("sel.first").split(".")[0])
            last_line = int(self.text.index("sel.last").split(".")[0])
            
            # Loop through all the selected lines and comment or uncomment them
            for line in range(first_line, last_line 1):
                if self.text.get("{}.0".format(line), "{}.1".format(line)) != "#":
                    self.text.insert("{}.0".format(line), "#")
                else:
                    self.text.delete("{}.0".format(line), "{}.1".format(line))
        except tkinter.TclError:
            
            # Get the line number of the current cursor position
            insert = self.text.index("insert").split(".")[0]
            
            # Comment or uncomment the current line
            if self.text.get("{}.0".format(insert), "{}.1".format(insert)) != "#":
                self.text.insert("{}.0".format(insert), "#")
            else:
                self.text.delete("{}.0".format(insert), "{}.1".format(insert))
        return "break"

if __name__ == "__main__":
    Window().mainloop()
  • Related