Problem in creating new class objects in Canvas and Text widgets made using Python and some tcl?

I foun a text editor in which tkinter.Canvas was added for line numbers and the tkinter.Text widget along with tkinter.Canvas was inside a tkinter.Frame and that Frame was to be added in a tkinter.ttk.Notebook tab. Here is the code for that which I cut out from the Notebook tab I was trying to add:

from tkinter import *
import tkinter as tk


class LineNumberCanvas(Canvas):
    def __init__(self, *args, **kwargs):
        Canvas.__init__(self, *args, **kwargs)
        self.text_widget = None
        self.breakpoints = []

    def connect(self, text_widget):
        self.text_widget = text_widget

    def re_render(self):
        """Re-render the line canvas"""
        self.delete('all') # To prevent drawing over the previous canvas

        temp = self.text_widget.index("@0, 0")
        while True :
            dline= self.text_widget.dlineinfo(temp)
            if dline is None: 
                break
            y = dline[1]
            x = dline[0]
            linenum = str(temp).split(".")[0]

            id = self.create_text(2, y, anchor="nw", text=linenum, font='Consolas 13')

            if int(linenum) in self.breakpoints:                
                x1, y1, x2, y2 = self.bbox(id)
                self.create_oval(x1, y1, x2, y2, fill='red')
                self.tag_raise(id)

            temp = self.text_widget.index("%s+1line" % temp)

    def get_breakpoint_number(self,event):
         if self.find_withtag('current'):
            i = self.find_withtag('current')[0]
            linenum = int(self.itemcget(i,'text'))

            if linenum in self.breakpoints:
                self.breakpoints.remove(linenum)
            else:
                self.breakpoints.append(linenum)
            self.re_render()
            
            

class CustomText:
    def __init__(self, text):
        self.text = text
        self.master = text.master
        self.mechanise()
        self._set_()
        self.binding_keys()

    def mechanise(self):
        self.text.tk.eval('''
            proc widget_interceptor {widget command args} {

                set orig_call [uplevel [linsert $args 0 $command]]

              if {
                    ([lindex $args 0] == "insert") ||
                    ([lindex $args 0] == "delete") ||
                    ([lindex $args 0] == "replace") ||
                    ([lrange $args 0 2] == {mark set insert}) || 
                    ([lrange $args 0 1] == {xview moveto}) ||
                    ([lrange $args 0 1] == {xview scroll}) ||
                    ([lrange $args 0 1] == {yview moveto}) ||
                    ([lrange $args 0 1] == {yview scroll})} {

                    event generate  $widget <<Changed>>
                }

                #return original command
                return $orig_call
            }
            ''')
        self.text.tk.eval('''
            rename {widget} new
            interp alias {{}} ::{widget} {{}} widget_interceptor {widget} new
        '''.format(widget=str(self.text)))
        return


    def binding_keys(self):
        for key in ['<Down>', '<Up>', "<<Changed>>", "<Configure>"]:
            self.text.bind(key, self.changed)
        self.linenumbers.bind('<Button-1>', self.linenumbers.get_breakpoint_number)
        return

    def changed(self, event):
        self.linenumbers.re_render()
        #print "render"
        return


    def _set_(self):
        self.linenumbers = LineNumberCanvas(self.master, width=30)
        self.linenumbers.connect(self.text)
        self.linenumbers.pack(side="left", fill="y")
        return       


if __name__ == '__main__':
    root = Tk()
    l = Text(root, font='Consolas 13')
    CustomText(l)
    l.pack(expand=TRUE, fill=BOTH)
    root.mainloop()

Now the problem here is that I don't know tcl language and the error that the program generates is in the mechanise function of CustomText class where there is 'new' keyword. It says:

  File "C:\Users\Prerak\AppData\Local\Programs\Python\Python37\EZ_PY\ColorText.py", line 590, in mechanise
    '''.format(widget=str(self)))
_tkinter.TclError: can't rename to "new": command already exists

Can anybody pls help me out in resolving this...and all I am doing is adding new tab after clicking on a button in which CustomText object is added to the tkinter.Notebook tab.

1 answer

  • answered 2020-10-16 07:42 Donal Fellows

    The problem is what it says it is: there's already a command in the Tcl interpreter called new. It's not part of the base Tcl command set, so it's probably coming from some package that has been loaded by default. Whatever it is, if it isn't yours, it probably needs to be left in place otherwise something else will break.

    The simplest method for handling this is to use a unique counter so that every intercepted widget gets something unique (this is analogous to what gensym does in Lisp). As this problem manifests on the Tcl side, you can keep that counter on that side, and the convenient way of doing that involves making a second procedure, which I'll call install_widget_interceptor. As a bonus, the call to make use of it becomes simpler and you become able to have two CustomText instances at once.

    (… omitting the code which does not change, and I've tidied up the widget_interceptor procedure to be a bit more idiomatic …)

        # You probably shouldn't repeat this bit every time you create a widget
        self.text.tk.eval('''
            proc widget_interceptor {widget command args} {
                set orig_call [uplevel 1 [linsert $args 0 $command]]
                if {
                    [lindex $args 0] in {insert delete replace} ||
                    ([lrange $args 0 2] == {mark set insert}) || 
                    ([lrange $args 0 1] == {xview moveto}) ||
                    ([lrange $args 0 1] == {xview scroll}) ||
                    ([lrange $args 0 1] == {yview moveto}) ||
                    ([lrange $args 0 1] == {yview scroll})
                } then {
                    event generate $widget <<Changed>>
                }
                #return original command
                return $orig_call
            }
            proc install_widget_interceptor {widget} {
                global unique_widget_id
                set handle ::_intercepted_widget_[incr unique_widget_id]
                rename $widget $handle
                interp alias {} ::$widget {} widget_interceptor $widget $handle
            }
            ''')
    
        # This bit you absolutely need each time
        self.text.tk.eval('''
            install_widget_interceptor {widget}
        '''.format(widget=str(self.text)))
    

    As noted in the code, the creation of the procedures on the Tcl side probably ought to be done just once, at the time your Python class is created. It's not critical to do it every time; just wasteful.

    Also, Tk itself generates <<Modified>> events for Text widgets when they are modified. It uses a different sense of what matters to you, focusing on changes to the model that it manages instead of to the view on that model (so moving the cursor or scrolling won't trigger it).