0

I'm making a Test Editor from a base that I got from a YouTube tutorial. I was trying to make highlighted the python's statements, but when I write a statement, it colorizes all the lines, and I thought that the problem is the use of indexes I make.

This is the code:

import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox


class Menubar:

    def __init__(self, parent):
        font_specs = 14
        
        menubar = tk.Menu(parent.master)
        parent.master.config(menu = menubar)

        file_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        file_dropdown.add_command(label = "Nuovo file",
                                  accelerator = "Ctrl + N",
                                  command = parent.new_file)

        file_dropdown.add_command(label = "Apri file",
                                  accelerator = "Ctrl + O",
                                  command = parent.open_file)

        file_dropdown.add_command(label = "Salva",
                                  accelerator = "Ctrl + S",
                                  command = parent.save)

        file_dropdown.add_command(label = "Salva con nome",
                                  accelerator = "Ctrl + Shit + S",
                                  command = parent.save_as)

        file_dropdown.add_separator()

        file_dropdown.add_command(label = "Esci",
                                  command = parent.master.destroy)

        about_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        about_dropdown.add_command(label = "Note di rilascio",
                                   command = self.show_about_message)

        about_dropdown.add_separator()

        about_dropdown.add_command(label = "About",
                                   command = self.show_release_notes)

        settings_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        settings_dropdown.add_command(label = "Cambia lo sfondo dell'editor",
                                      command = parent.change_background)

        menubar.add_cascade(label = "File", menu = file_dropdown)
        menubar.add_cascade(label = "About", menu = about_dropdown)
        menubar.add_cascade(label = "Settings", menu = settings_dropdown)


    def show_about_message(self):
        box_title = "Riguardo PyText"
        box_message = "Il mio primo editor testuale creato con Python e TkInter!"

        messagebox.showinfo(box_title, box_message)


    def show_release_notes(self):
        box_title = "Note di Rilascio"
        box_message = "Versione 0.1 (Beta) Santa"

        messagebox.showinfo(box_title, box_message)
        

class Statusbar:

    def __init__(self, parent):
        font_specs = 12

        self.status = tk.StringVar()
        self.status.set("PyText - 0.1 Santa")

        label = tk.Label(parent.text_area, textvariable = self.status,
                         fg = "black", bg = "lightgrey", anchor = "sw")

        label.pack(side = tk.BOTTOM, fill = tk.BOTH)

    def update_status(self, *args):
        if isinstance(args[0], bool):
            self.status.set("Il tuo file è stato salvato!")

        else:
            self.status.set("PyText - 0.1 Santa")


class PyText:
    """
    Classe-Madre dell'applicazione
    """

    def __init__(self, master):
        master.title("Untitled - PyText")
        master.geometry("1200x700")

        font_specs = 18
        
        self.master = master
        self.filename = None
        
        self.text_area = tk.Text(master, font = font_specs, insertbackground = "black")
        self.scroll = tk.Scrollbar(master, command = self.text_area.yview)
        self.text_area.configure(yscrollcommand = self.scroll.set)
        self.text_area.pack(side = tk.LEFT, fill = tk.BOTH, expand = True)
        self.scroll.pack(side = tk.RIGHT, fill = tk.Y)

        self.menubar = Menubar(self)
        self.statusbar = Statusbar(self)
        self.bind_shortcuts()

        
    def set_window_title(self, name = None):
        if name:
            self.master.title(name + " - PyText")
            
        else:
            self.master.title("Untitled - PyText")    


    def new_file(self, *args):
        self.text_area.delete(1.0, tk.END)
        self.filename = None

        self.set_window_title()


    def open_file(self, *args):
        self.filename = filedialog.askopenfilename(
            defaultextension = ".txt",
            filetypes = [("Tutti i file", "*.*"),
                         ("File di Testo", "*.txt"),
                         ("Script Python", "*.py"),
                         ("Markdown Text", "*.md"),
                         ("File JavaScript", "*.js"),
                         ("Documenti Html", "*.html"),
                         ("Documenti CSS", "*.css"),
                         ("Programmi Java", "*.java")]
        )

        if self.filename:
            self.text_area.delete(1.0, tk.END)

            with open(self.filename, "r") as f:
                self.text_area.insert(1.0, f.read())

            self.set_window_title(self.filename)


    def save(self, *args):
        if self.filename:
            try:
                textarea_content = self.text_area.get(1.0, tk.END)
                with open(self.filename, "w") as f:
                    f.write(textarea_content)

                self.statusbar.update_status(True)

            except Exception as e:
                print(e)

        else:
            self.save_as()


    def save_as(self, *args):
        try:
            new_file = filedialog.asksaveasfilename(
                initialfile = "Untitled.txt",
                defaultextension = ".txt",
                filetypes = [("Tutti i file", "*.*"),
                             ("File di Testo", "*.txt"),
                             ("Script Python", "*.py"),
                             ("Markdown Text", "*.md"),
                             ("File JavaScript", "*.js"),
                             ("Documenti Html", "*.html"),
                             ("Documenti CSS", "*.css"),
                             ("Programmi Java", "*.java")]
                )

            textarea_content = self.text_area.get(1.0, tk.END)
            with open(new_file, "w") as f:
                f.write(textarea_content)

            self.filename = new_file
            self.set_window_title(self.filename)
            self.statusbar.update_status(True)

        except Exception as e:
            print(e)


    def change_background(self):
        self.text_area.config(background = "black", foreground = "white", 
                              insertbackground = "white", insertwidth = 2)

        
    def highlight_text(self, *args):
        tags = {
            "import": "pink",
            "from": "pink",
            "def": "blue",
            "for": "purple",
            "while": "purple"
        }

        textarea_content = self.text_area.get(1.0, tk.END)
        lines = textarea_content.split("\n")

        for row in lines:
            for tag in tags:
                index = row.find(tag) + 1.0

                if index > 0.0:
                    self.text_area.tag_add(tag, index, index + len(tag) -1)
                    self.text_area.tag_config(tag, foreground = tags.get(tag))

                    print("Nuovo tag aggiunto:", tag)
            
        print("Funzione eseguita:", args, "\n")
    
    
    def bind_shortcuts(self):
        self.text_area.bind("<Control-n>", self.new_file)
        self.text_area.bind("<Control-o>", self.open_file)
        self.text_area.bind("<Control-s>", self.save)
        self.text_area.bind("<Control-S>", self.save_as)
        self.text_area.bind("<Key>", self.highlight_text, self.statusbar.update_status)

    
if __name__ == "__main__":
    master = tk.Tk()
    pt = PyText(master)
    master.mainloop()

How can I get the index of the line where is the statement?

furas
  • 134,197
  • 12
  • 106
  • 148
Seintian
  • 151
  • 1
  • 1
  • 11
  • Please reduce the code down to a [mcve]. If the question is about indexing a text widget, we don't need much more than the text widget. All of the other functions are not necessary to reproduce the problem. – Bryan Oakley Dec 11 '20 at 12:56
  • Right, sorry guys, I'll keep this in mind, thank you :3 – Seintian Dec 11 '20 at 14:01

1 Answers1

1

You works with every line separtelly so you get only X. To get Y you need to enumerate lines:

for y, row in enumerate(lines, 1):

Result from find() you convert to float but you need int and later convert X,Y to string "Y.X"

start = '{}.{}'.format(y, x)
end   = '{}.{}'.format(y, x+len(tag))

Version which works for me

for y, row in enumerate(lines, 1):
    for tag in tags:

        x = row.find(tag)

        if x > -1:
            print(f"{tag} | x: {x} | y: {y}")

            start = '{}.{}'.format(y, x)
            end   = '{}.{}'.format(y, x+len(tag))

            print(f"{tag} | start: {start} | end: {end}")

            self.text_area.tag_add(tag, start, end)
            self.text_area.tag_config(tag, foreground = tags.get(tag))

Your idea has only one big problem - it will colorize def in word define, or for in word forward, etc. So maybe it would need regex to create more complex rules and search only full words.

Other problem: find() gives you only first item in line - so if you will try to highlight element which can be two times in line then you would have to use loop with find(..., start) to search after first element.

Maybe it would be better to use example for How to highlight text in a tkinter Text widget


BTW:

You binded key shortcuts to self.text_area so I had to click in text area to use shortcut. If I change self.text_area.bind into self.master.bind then shortcuts works even without clicking in text area.


EDIT:

There is Thonny IDE which uses Tkinter and it highlights code.

I tried to find how it makes this but I found only regex for highlights - thonny_utils.py

Maybe if you get full code and use some tool to search string in files (like grep in Linux) then you could find all places where it uses variables from token_utils.py (ie. KEYWORD)

EDIT: coloring.py


EDIT:

Full code with function highlight_text which uses previous method line.find and highlight_text_regex which uses text_area.search with regex.

New version based on code from answer to question How to highlight text in a tkinter Text widget

enter image description here

import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
import os

print(os.getcwd())

class Menubar:

    def __init__(self, parent):
        font_specs = 14
        
        menubar = tk.Menu(parent.master)
        parent.master.config(menu = menubar)

        file_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        file_dropdown.add_command(label = "Nuovo file",
                                  accelerator = "Ctrl + N",
                                  command = parent.new_file)

        file_dropdown.add_command(label = "Apri file",
                                  accelerator = "Ctrl + O",
                                  command = parent.open_file)

        file_dropdown.add_command(label = "Salva",
                                  accelerator = "Ctrl + S",
                                  command = parent.save)

        file_dropdown.add_command(label = "Salva con nome",
                                  accelerator = "Ctrl + Shift + S",
                                  command = parent.save_as)

        file_dropdown.add_separator()

        file_dropdown.add_command(label = "Esci",
                                  accelerator = "Ctrl + Q",
                                  command = parent.master.destroy)

        about_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        about_dropdown.add_command(label = "Note di rilascio",
                                   command = self.show_about_message)

        about_dropdown.add_separator()

        about_dropdown.add_command(label = "About",
                                   command = self.show_release_notes)

        settings_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        settings_dropdown.add_command(label = "Cambia lo sfondo dell'editor",
                                      command = parent.change_background)

        menubar.add_cascade(label = "File", menu = file_dropdown)
        menubar.add_cascade(label = "About", menu = about_dropdown)
        menubar.add_cascade(label = "Settings", menu = settings_dropdown)


    def show_about_message(self):
        box_title = "Riguardo PyText"
        box_message = "Il mio primo editor testuale creato con Python e TkInter!"

        messagebox.showinfo(box_title, box_message)


    def show_release_notes(self):
        box_title = "Note di Rilascio"
        box_message = "Versione 0.1 (Beta) Santa"

        messagebox.showinfo(box_title, box_message)
        

class Statusbar:

    def __init__(self, parent):
        font_specs = 12

        self.status = tk.StringVar()
        self.status.set("PyText - 0.1 Santa")

        label = tk.Label(parent.text_area, textvariable = self.status,
                         fg = "black", bg = "lightgrey", anchor = "sw")

        label.pack(side = tk.BOTTOM, fill = tk.BOTH)

    def update_status(self, *args):
        if isinstance(args[0], bool):
            self.status.set("Il tuo file è stato salvato!")

        else:
            self.status.set("PyText - 0.1 Santa")


class PyText:
    """
    Classe-Madre dell'applicazione
    """

    def __init__(self, master):
        master.title("Untitled - PyText")
        master.geometry("1200x700")

        font_specs = 18
        
        self.master = master
        self.filename = None
        
        self.text_area = tk.Text(master, font = font_specs, insertbackground = "black")
        self.scroll = tk.Scrollbar(master, command = self.text_area.yview)
        self.text_area.configure(yscrollcommand = self.scroll.set)
        self.text_area.pack(side = tk.LEFT, fill = tk.BOTH, expand = True)
        self.scroll.pack(side = tk.RIGHT, fill = tk.Y)

        self.menubar = Menubar(self)
        self.statusbar = Statusbar(self)
        self.bind_shortcuts()

        
    def set_window_title(self, name = None):
        if name:
            self.master.title(name + " - PyText")
            
        else:
            self.master.title("Untitled - PyText")    


    def new_file(self, *args):
        self.text_area.delete(1.0, tk.END)
        self.filename = None

        self.set_window_title()


    def open_file(self, *args):
        self.filename = filedialog.askopenfilename(
            initialdir = os.getcwd(),
            defaultextension = ".txt",
            filetypes = [("Tutti i file", "*.*"),
                         ("File di Testo", "*.txt"),
                         ("Script Python", "*.py"),
                         ("Markdown Text", "*.md"),
                         ("File JavaScript", "*.js"),
                         ("Documenti Html", "*.html"),
                         ("Documenti CSS", "*.css"),
                         ("Programmi Java", "*.java")]
        )

        if self.filename:
            self.text_area.delete(1.0, tk.END)

            with open(self.filename, "r") as f:
                self.text_area.insert(1.0, f.read())

            self.set_window_title(self.filename)


    def save(self, *args):
        if self.filename:
            try:
                textarea_content = self.text_area.get(1.0, tk.END)
                with open(self.filename, "w") as f:
                    f.write(textarea_content)

                self.statusbar.update_status(True)

            except Exception as e:
                print(e)

        else:
            self.save_as()


    def save_as(self, *args):
        try:
            new_file = filedialog.asksaveasfilename(
                initialfile = "Untitled.txt",
                defaultextension = ".txt",
                filetypes = [("Tutti i file", "*.*"),
                             ("File di Testo", "*.txt"),
                             ("Script Python", "*.py"),
                             ("Markdown Text", "*.md"),
                             ("File JavaScript", "*.js"),
                             ("Documenti Html", "*.html"),
                             ("Documenti CSS", "*.css"),
                             ("Programmi Java", "*.java")]
                )

            textarea_content = self.text_area.get(1.0, tk.END)
            with open(new_file, "w") as f:
                f.write(textarea_content)

            self.filename = new_file
            self.set_window_title(self.filename)
            self.statusbar.update_status(True)

        except Exception as e:
            print(e)


    def change_background(self):
        self.text_area.config(background = "black", foreground = "white", 
                              insertbackground = "white", insertwidth = 2)

        
    def highlight_text_old(self, *args):
        tags = {
            "import": "pink",
            "from": "red",
            "def": "blue",
            "for": "purple",
            "while": "green",
        }

        textarea_content = self.text_area.get(1.0, tk.END)
        lines = textarea_content.split("\n")

        for y, row in enumerate(lines, 1):
            for tag in tags:
                x = row.find(tag)
                if x > -1:
                    print(f"{tag} | x: {x} | y: {y}")
                    start = '{}.{}'.format(y, x)
                    end   = '{}.{}'.format(y, x+len(tag))
                    print(f"{tag} | start: {start} | end: {end}")
                    self.text_area.tag_add(tag, start, end)
                    self.text_area.tag_config(tag, foreground = tags.get(tag))

                    #print("Nuovo tag aggiunto:", tag)
            
        #print("Funzione eseguita:", args, "\n")

    def highlight_text(self, *args):
        # TODO: move to `__init__` ?
        tags = {
            "import": "pink",
            "from": "red",
            "def": "blue",
            "for": "purple",
            "while": "green",
        }

        # TODO: move to `__init__` ?
        # create tags with assigned color - do it only onve (in __init__)
        for color in ['pink', 'red', 'blue', 'purple', 'green']:
            self.text_area.tag_config(color, foreground=color)

        # remove all tags from text
        for tag in self.text_area.tag_names():
            self.text_area.tag_remove(tag, '1.0', 'end')  # not `tag_remove()`
         

        textarea_content = self.text_area.get(1.0, tk.END)
        lines = textarea_content.split("\n")

        for y, row in enumerate(lines, 1):
            for tag in tags:
                x = row.find(tag)
                if x > -1:
                    print(f"{tag} | x: {x} | y: {y}")
                    match_start = '{}.{}'.format(y, x)
                    match_end   = '{}.{}'.format(y, x+len(tag))
                    print(f"{tag} | start: {match_start} | end: {match_end}")
                    self.text_area.tag_add(tag, match_start, match_end)
                    #self.text_area.tag_config(tag, foreground=tags.get(tag))  # create tags only once - at start

                    #print("Nuovo tag aggiunto:", tag)
            
        #print("Funzione eseguita:", args, "\n")


    def highlight_text_regex(self, *args):
        # TODO: move to `__init__` ?
        tags = {
            "import": "red",
            "from": "red",
            "as": "red",

            "def": "blue",
            "class": "blue",

            "for": "green",
            "while": "green",

            "if": "brown",
            "elif": "brown",
            "else": "brown",

            "print": "purple",            

            "True": "blue",
            "False": "blue",
            "self": "blue",

            "\d+": "red",  # digits

            "__[a-zA-Z][a-zA-Z0-9_]*__": "red",  # ie. `__init__`
        }

        # add `\m \M` to words
        tags = {f'\m{word}\M': tag for word, tag in tags.items()}

        # tags which doesn't work with  `\m \M`
        other_tags = {
            "\(": "brown",  # need `\` because `(` has special meaning
            "\)": "brown",  # need `\` because `)` has special meaning

            ">=": "green",
            "<=": "green",
            "=": "green",
            ">": "green",
            "<": "green",

            "#.*$": "brown",  # comment - to the end of line `$`
        }

        # create one dictionary with all tags
        tags.update(other_tags)

        # TODO: move to `__init__` ?
        # create tags with assigned color - do it only onve (in __init__)
        for color in ['pink', 'red', 'blue', 'purple', 'green', 'brown', 'yellow']:
            self.text_area.tag_config(color, foreground=color)

        # remove all tags from text before adding all tags again (to change color when ie. `def` change to `define`)
        for tag in self.text_area.tag_names():
            self.text_area.tag_remove(tag, '1.0', 'end')  # not `tag_remove()`

        count_chars = tk.IntVar() # needs to count matched chars - ie. for digits `\d+`
        # search `word` and add `tag`
        for word, tag in tags.items():
            #pattern = f'\m{word}\M'  # http://tcl.tk/man/tcl8.5/TclCmd/re_syntax.htm#M72
            pattern = word  # http://tcl.tk/man/tcl8.5/TclCmd/re_syntax.htm#M72
            search_start = '1.0'
            search_end   = 'end'
            while True:
                position = self.text_area.search(pattern, search_start, search_end, count=count_chars, regexp=True)
                print('search:', word, position)
                if position:
                    print(f"{word} | pos: {position}")
                    match_start = position
                    match_end   = '{}+{}c'.format(position, count_chars.get()) #len(word)) # use special string `Y.X+Nc` instead values (Y, X+N)
                    print(f"{word} | start: {match_start} | end: {match_end}")
                    self.text_area.tag_add(tag, match_start, match_end)
                    #self.text_area.tag_config(tag, foreground=tags.get(tag))  # create tags only once - at start
                    search_start = match_end  # to search next word
                else:
                    break    

    def quit(self, *args):
        self.master.destroy()

    def bind_shortcuts(self):
        self.master.bind("<Control-n>", self.new_file)
        self.master.bind("<Control-o>", self.open_file)
        self.master.bind("<Control-s>", self.save)
        self.master.bind("<Control-S>", self.save_as)
        self.master.bind("<Control-q>", self.quit)
        self.master.bind("<Key>", self.highlight_text_regex, self.statusbar.update_status)

    
if __name__ == "__main__":
    master = tk.Tk()
    pt = PyText(master)
    master.mainloop()
furas
  • 134,197
  • 12
  • 106
  • 148
  • Thank you so much for your exhaustive answer! To give more sense to my method .find could I put the statements following a space? (ex. "def " so it won't confuse "define" with "def") For the bindings, yes, it would be better ahah For the last advice I tried to read first link but I didn't understand much, and in the second link I read the code but I didn't understand the same :/ maybe I have to code a little more to understand all these things. Btw, I'll implement the second loop for the .find() and... that's it ;P Thank you for the answer, man! :D – Seintian Dec 11 '20 at 14:01
  • maybe you should add space in both sides ` def ` to skip `define` but also words like `undef`. But this makes problem when `def` is at the beginning of line or someone uses tabs instead of spaces to make indentations. Similar problem will be when someone will not use space after word - ie. `while(1)`. Using `regex` with special char `\b` you can catch all these situations - `re.search("\bdef\b", ...)`. In regex you can also use one pattern for all words which use the same color `re.search("from|import", ...)` - and this method I see in regex in Thonny (`thonny_utils.py`) – furas Dec 11 '20 at 15:12
  • Sorry, can you post the code with regex? I searched online for this because I've never heard about it before today but I can't understand how does it functions. I'll ask my prof to explain me this new argument O.o – Seintian Dec 12 '20 at 18:13
  • I added full code to answer - there is previous `highlight_text` and `highlight_text_regex` – furas Dec 13 '20 at 02:14
  • oh ok, I read the methods you added and I'm trying to understand them... Yeah, you added other tags and regex too... you took the pairs of tags - colors and put the tags in patterns with "\m" and "\M"... and then you created the tags in the text_area... Well, I think I understood more or less everything, thank you very much, man! Now I understood also what are regexes ;) But... last question... what does it mean "TODO" in comments? and those comments "TODO: move to '__init__'?" – Seintian Dec 13 '20 at 11:37
  • `TODO` means `(eventually) TO DO it later`. And `TODO: move to __init__` means that this code could be executed once - ie. in `__init__`. There is no need to execute it many times - there is no need to define `tags` many times because they never changes - so you could do it only once in `__init__`. There is no need to define ` self.text_area.tag_config` many times because they never change - so you could do it only once in `__init__` – furas Dec 13 '20 at 17:44
  • ah ok ok, I understood. Thank you man for all, really <3 – Seintian Dec 13 '20 at 18:38