2

I'm writing my own code editor and I want it to have numbered lines on left side. Based on this answer I wrote this sample code:

#!/usr/bin/env python3

import tkinter


class CodeEditor(tkinter.Frame):
    def __init__(self, root):
        tkinter.Frame.__init__(self, root)
        # Line numbers widget
        self.__line_numbers_canvas = tkinter.Canvas(self, width=40, bg='#555555', highlightbackground='#555555', highlightthickness=0)
        self.__line_numbers_canvas.pack(side=tkinter.LEFT, fill=tkinter.Y)

        self.__text = tkinter.Text(self)
        self.__text['insertbackground'] = '#ffffff'
        self.__text.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True)

    def __update_line_numbers(self):
        self.__line_numbers_canvas.delete("all")
        i = self.__text.index('@0,0')
        self.__text.update()                    #FIX: adding line
        while True:
            dline = self.__text.dlineinfo(i)
            if dline:
                y = dline[1]
                linenum = i[0]
                self.__line_numbers_canvas.create_text(1, y, anchor="nw", text=linenum, fill='#ffffff')
                i = self.__text.index('{0}+1line'.format(i))  #FIX
            else:
                break

    def load_from_file(self, path):
        self.__text.delete('1.0', tkinter.END)
        f = open(path, 'r')
        self.__text.insert('0.0', f.read())
        f.close()
        self.__update_line_numbers()


class Application(tkinter.Tk):
    def __init__(self):
        tkinter.Tk.__init__(self)
        code_editor = CodeEditor(self)
        code_editor.pack(fill=tkinter.BOTH, expand=True)
        code_editor.load_from_file(__file__)

    def run(self):
        self.mainloop()


if __name__ == '__main__':
    app = Application()
    app.run()

Unfortunately something is wrong inside __update_line_numbers. This method should write line numbers from top to bottom on my Canvas widget but it prints only the number for the first line (1) and then exits. Why?

Community
  • 1
  • 1
BPS
  • 1,133
  • 1
  • 17
  • 38
  • Do you have any text in all lines ? – furas Jul 22 '14 at 20:08
  • @furas Yes, this example is ready to run and it loads his own source code. – BPS Jul 22 '14 at 20:10
  • @BPS: Well, yeah, but line 2 of the source code is empty; if you remove that blank line, does it now number the first 2 lines then stop, or still just the first? – abarnert Jul 22 '14 at 20:18
  • Removing empty lines from text changes nothing. – BPS Jul 22 '14 at 20:21
  • OK, two more things for you to test: If you `print(self.__text.dump(previndex, i))` (obviously you need to add `previndex=i` right before changing it, and set it to something before the loop), are you getting anything for line 2? Does just `dlineindex('2.0')` return `None`, or a value? – abarnert Jul 22 '14 at 20:24
  • Oh, also, the `dlineinfo` docs explicitly say "This method only works if the text widget is updated. To make sure this is the case, you can call the `update_idletasks` method first." Your code isn't doing that. – abarnert Jul 22 '14 at 20:24

2 Answers2

1

The root problem is that you're calling dlineinfo before returning to the runloop, so the text hasn't been laid out yet.

As the docs explain:

This method only works if the text widget is updated. To make sure this is the case, you can call the update_idletasks method first.

As usual, to get more information, you have to turn to the Tcl docs for the underlying object, which basically tell you that the Text widget may not be correct about which characters are and are not visible until it's updated, in which case it may be returning None not because of any problem, but just because, as far as it's concerned, you're asking for the bbox of something that's off-screen.

A good way to test whether this is the problem is to call self.__text.see(i) before calling dlineinfo(i). If it changes the result of dlineinfo, this was the problem. (Or, if not that, at least something related to that—for whatever reason, Tk thinks everything after line 1 is off-screen.)

But in this case, even calling update_idletasks doesn't work, because it's not just updating the line info that needs to happen, but laying out the text in the first place. What you need to do is explicitly defer this call. For example, add this line to the bottom of load_from_file and now it works:

self.__text.after(0, self.__update_line_numbers)

You could also call self.__text.update() before calling self.__update_line_numbers() inline, and I think that should work.


As a side note, it would really help you to either run this under the debugger, or add a print(i, dline) at the top of the loop, so you can see what you're getting, instead of just guessing.

Also wouldn't it be easier to just increment a linenumber and use '{}.0'.format(linenumber) instead of creating complex indexes like @0,0+1line+1line+1line that (at least for me) don't work. You can call Text.index() to convert any index to canonical format, but why make it so difficult? You know that what you want is 1.0, 2.0, 3.0, etc., right?

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • I could replace that line to: i = '{0}.0'.format(int(i[0])+1), then next indexes would be 2.0, 3.0, 4.0, ... Its still not working. – BPS Jul 22 '14 at 20:14
  • @BPS: Are you sure you actually have multiple lines, rather than, e.g., one giant wrapped line? (What do you get if you convert `END` to line.col format?) – abarnert Jul 22 '14 at 20:16
  • You are incorrect. "1.0+1line" is a perfectly acceptable index. That is not the root cause of the problem. – Bryan Oakley Jul 22 '14 at 20:33
  • @BryanOakley: When I pass in `@0,0+1line+1line+1line+1line` it doesn't like the index. – abarnert Jul 22 '14 at 20:42
  • @abarnert: you must be doing something wrong. While ugly, `@0,0+1line+1line+1line` is perfectly acceptable and is documented as such. – Bryan Oakley Jul 22 '14 at 20:49
  • @BryanOakley: Did you downvote this answer because you don't like the way I described a side issue? I can remove the "that (at least for me) don't work" if it makes a difference. – abarnert Jul 22 '14 at 21:39
  • @abarnert: I downvoted because the original answer was wrong and misleading. The current version at the time I write this is much better. – Bryan Oakley Jul 22 '14 at 22:58
1

The root cause of the problem is that the text hasn't been drawn on the screen yet, so the call to dlineinfo will not return anything useful.

If you add a call to self.update() before drawing the line numbers, your code will work a little better. It won't work perfectly, because you have other bugs. Even better, call the function when the GUI goes idle, or on a Visibility event or something like that. A good rule of thumb is to never call update unless you understand why you should never call update(). In this case, however, it's relatively harmless.

Another problem is that you keep appending to i, but always use i[0] when writing to the canvas. When you get to line 2, i will be "1.0+1line". For line three it will be "1.0+1line+1line", and so on. The first character will always be "1".

What you should be doing is asking tkinter to convert your modified i to a canonical index, and using that for the line number. For example:

i = self.__text.index('{0}+1line'.format(i))

This will convert "1.0+1line" to "2.0", and "2.0+1line" to "3.0" and so on.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685