1

I'm building a bulleted list text widget with python and tkinter. I'd like the bullets themselves to be "non-editable", meaning the user can't delete the actual bullet character. I've created a simple example to show the problem. In my example I add a bullet ("-") and then two spaces. The user can backspace and delete the bullet and the two spaces I added, but I don't want them to be able to. I want them to be able to backspace only up to the "B" in "Bullet example".

I thought that by using two tags, one for the bullet, and one for the text on the bulleted line, i'd be able to do that. It sort of works. The bullet tag adds a left margin that the user can't delete past, but the bullet character and the spaces i'm using for padding are unfortunately still deletable.

How can I create a non-deletable bullet (with a margin on its left and right) within a text widget that is editable by the user?

Thanks!

enter image description here

import tkinter as tk
from tkinter import font as font


class BulletText(tk.Text):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.bullet_char = '-'
        self.num_spaces_per_indent = 4
        self.num_spaces_after_bullet = 2
        font_name = font.nametofont(self.cget("font"))
        self.indent_width = font_name.measure(self.num_spaces_per_indent * ' ')
        self.bullet_width = font_name.measure(f'{self.bullet_char}{self.num_spaces_after_bullet * " "}')

        self.tag_configure('bullet', lmargin1=self.indent_width, background='red')
        self.tag_configure('bullet_text', lmargin1=self.indent_width, lmargin2=self.indent_width + self.bullet_width,
                           background='green')

    def insert_bullet(self, position, text):
        # convert line and character to int
        line = int(position.split('.')[0])
        col = int(position.split('.')[1])

        self.insert(f'{line}.0', self.bullet_char)
        self.tag_add('bullet',f'{line}.0',f'{line}.1')
        self.insert(f'{line}.1', f'{self.num_spaces_after_bullet*" "}')
        self.tag_add('bullet_text', f'{line}.1', f'{line}.end+1c')
        self.insert(f'{line}.3', text)


if __name__ == "__main__":
    root = tk.Tk()
    text = BulletText(root, width=40, height=15)
    text.pack(fill="both", expand=True)

    position = text.index('insert')
    text.insert_bullet(position, "Bullet example")

    root.mainloop()

Brett Elliot
  • 922
  • 1
  • 6
  • 9
  • I think yesterday was similar question - or maybe in question about blocking `Ctr+C`/`Ctrl+V` was link to older question which was using `tags` to control text. – furas Dec 28 '20 at 05:14
  • 1
    [How can you mark a portion of a text widget as readonly?](https://stackoverflow.com/questions/11180130/how-can-you-mark-a-portion-of-a-text-widget-as-readonly) – furas Dec 28 '20 at 05:17

1 Answers1

1

There are two possible solutions to your problem that I am aware of.

  1. You could have a function that checks the current input-value of your BulletText at positions [0], [1], and [2] for "- ", and then reinserts them if they aren't there, or

  2. You could put a label object right before your BulletText in-line. This would not only prevent the user from removing the spaces and bullet, but would also prevent the user from selecting them in general, and thus requires no checks or extra functions. I think this is probably the best option. You could then use geometry to align the label and text containers however you like.

I hope my response was helpful!

JohnAlexINL
  • 615
  • 7
  • 15
  • 1
    OK, I chose option 1 and that was the simplest. Wish I could answer my own question, but here's the code: def on_backspace(self, event): # current position - 1 (this is before insertion) position = self.index('insert') # convert line and character to int line = int(position.split('.')[0]) col = int(position.split('.')[1]) # Get all the tags for the current cursor position tags = self.tag_names(position) if any("bullet" in s for s in tags): if col in [0,1,2,3]: return 'break' – Brett Elliot Dec 28 '20 at 15:45