Tkinter Text Widget: Multiline Blinking Caret For Box-Select

Question:

I’ve created a box-select feature for tk.Text. The widget gets the font height and concocts an .xbm image from it. The .xbm is used as a faux-caret, via image_create, for all selected lines except the line the real caret is on.

How do I make the faux-caret image instance(s) blink in time with the real caret?

or

What is another direction I can go to get these results?

Asked By: OneMadGypsy

||

Answers:

My solution was to get rid of the real caret visibly, draw a faux caret on every line, and then call a function that keeps swapping out an ‘on’ faux-caret with an ‘off’ faux-caret. Below are some relevant code snippets. Note: I actually have 3 faux-carets because I want the caret that is on the true active line to be a little darker.

    #FAUX-CARET
    #concoct, load, assign, and delete xbm for faux-caret
    def __loadcarets(self) -> None:
        #store insert on/off times for faux-caret `.after` calls
        self.__instime = (self['insertofftime'], self['insertontime'])
        
        fh = self.__fh    #font height from 'linespace'
        
        #make a temp xbm file
        with tempfile.NamedTemporaryFile(mode='w+b', suffix='.xbm', delete=False) as f:
            #create prettyprint xbm data
            xbmdata = ',nt'.join(','.join('0xFF' for _ in range(min(8, fh-(8*i)))) for i in range(math.ceil(fh/8)))
            #write xbm
            f.write((f"#define image_width {INSWIDTH}n#define image_height {fh}n"
                     "static unsigned char image_bits[] = {nt"
                     f'{xbmdata}}};').encode())
                     
        #load xbm files for faux-caret ~ they have to be in this order 
        #I gave this a placeholder name because you should never have to access this directly
        #__fauxcaret does everything          
        self.__  = (tk.BitmapImage(file=f.name, foreground='#999999'),                   #shadow caret 
                    tk.BitmapImage(file=f.name, foreground=self['background']),          #off caret
                    tk.BitmapImage(file=f.name, foreground=self['insertbackground']))    #main caret  
        
        #delete file
        os.unlink(f.name)
    
    #faux-caret create or config
    def __fauxcaret(self, index:str, on_bool=True, main_bool=False, cfg_bool=False) -> None:
        (self.image_create, self.image_configure)[cfg](index, image=self.__[(main<<on)|(on^1)])
    
    #blink the faux-caret(s)
    def __blink(self, on_bool=True):
        #nothing to do
        if not self.__boxselect: return
        
        #so we only do this once per first blink
        if not self.__blinksrt:
            #sort image indexes
            self.__blinksrt = sorted((self.index(n) for n in self.image_names()), key=float)
            
        if idx:=self.__blinksrt:
            #flip `on`
            on = not on
            #consider direction in forward perspective
            fw = not self.__lbounds.rv
            #reconfigure all carets
            for i in idx: self.__fauxcaret(i, on=on, cfg=True)
            #reconfigure "active line" caret, if off it will assign off again
            self.__fauxcaret(idx[-fw], on=on, main=True, cfg=True)
            #schedule next call
            self.__blinkid = self.after(self.__instime[on], self.__blink, on)
            return
            
        raise ValueError('__blink: Nothing to sort!')
    
    #reset blink
    def __blinkreset(self) -> None:
        #cancel after
        self.after_cancel(self.__blinkid)
        #reset blinksort
        self.__blinksrt  = None
   
Answered By: OneMadGypsy
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.