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()