1

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
  • Out of curiosity, can you show me where you copied this code from? Commented Oct 16, 2020 at 13:45

1 Answer 1

3

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

3
  • Awesome it worked great, and also great explanation. Many Thanks Sir
    – prerakl123
    Commented Oct 17, 2020 at 4:29
  • Note that the code is formally incomplete, in that it doesn't tidy up everything correctly when the widget is deleted. Such tidying up is… rather more code. Not that this matters if the essential lifetime of the widgets is your whole application; everything automatically cleans up properly on exit, of course. Commented Oct 17, 2020 at 8:03
  • The lifetime isn't the whole application. Do you mean even after the notebook tab is closed the class object will still be running in background ? I mean in the all the objects inside the tab should be destroyed on <<NotebookTabClosed>> event. Can you please explain a bit more
    – prerakl123
    Commented Oct 19, 2020 at 3:26

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.