Python tk. Setting widget focus when using tab key

Viewed 629

I have a GUI with a column of buttons on the left side, text entry box on the right and a footer with some generic GUI buttons (new/delete/save/exit). The buttons on the left side each feed a set of words to the text entry widget on the right. I'd like to be able to use control-tab or control-shift-tab when in the text entry widget and return focus to the button feeding the text entry widget. I've googled, searched and experimented but I'm not able to find a way to force focus on a specific button after using a tab key combination.

from tkinter import *
import decimal

class category_rule():
    def __init__(self,hi_range,low_range,word_list,rule,row):
        self.hi_range = decimal.Decimal(hi_range)
        self.low_range = decimal.Decimal(low_range)
        self.word_list = sorted(word_list)
        self.rule = rule
        self.button_row = row
        self.rule_button = None
        self.rule_entry_window=None

def build_rules():
    rule_dict = {}
    rule_index = {}
    words = []

    rule='Fruit'
    words=['Apple','Banana','Strawberry']
    row=0 
    rule_dict[rule] = category_rule('9999.99','0.00',words,rule,row)
    rule_index[row] = rule

    rule = 'Vegetable'
    words=['Asparagus','Brussel Sprouts','Kale','Tomato','Spinach']
    row=1
    rule_dict[rule] = category_rule('9999.99','0.00',words,rule,row)
    rule_index[row] = rule

    rule = 'Dessert'
    words=['Cake','Pudding','Cookies','Pie']
    row=2
    rule_dict[rule] = category_rule('9999.99','0.00',words,rule,row)
    rule_index[row] = rule

    return rule_dict, rule_index


class rule_entry_window:
    active_index = 0
    active_rule = None
    first_rule=None
    button_row=0
    rule_dict = {}
    rule_index = {}


    ''' methods to create the window, build the frames add the widgets '''
    def init_window_and_frames(self):   
        self.win = Tk()
        self.bg_color = '#134E9C'
        self.text_entry_frame = Frame(self.win, width=150, height=380, bg='#DDDECE', padx=1, pady=1, borderwidth=1)
        self.button_frame_outer = Frame(self.win, width=150, height=300, bg=self.bg_color, padx=3, pady=3, borderwidth=1)
        self.button_frame = Frame(self.button_frame_outer, width=150, height=300, bg = self.bg_color, padx=3, pady=3, borderwidth=1)
        self.range_entry_frame = Frame(self.win, width=150, height=40, bg=self.bg_color, padx=3, pady=3, borderwidth=1)
        self.footer_frame = Frame(self.win, width=480, height=30, bg=self.bg_color, padx=3, pady=3, borderwidth=1)

        self.button_frame.grid(row=0,column=0,sticky='nesw')
        self.button_frame_outer.grid(rowspan=2,column=0,sticky='nsew')
        self.text_entry_frame.grid(row=1,column=1,sticky='s')

        self.range_entry_frame.grid(row=0,column=1,sticky='new')
        self.footer_frame.grid(row=2,columnspan=2,sticky='ew')
        self.button_frame_outer.columnconfigure(0, minsize=200, weight=1)
        self.button_frame_outer.rowconfigure(0, weight=1)

    def init_rule_widgets(self):
        self.scrollbar = Scrollbar(self.text_entry_frame)
        self.text_entry = Text(self.text_entry_frame, width=40, height=33, wrap="word",
                   yscrollcommand=self.scrollbar.set, bg='#DDDECE',
                   borderwidth=0, highlightthickness=0)
        self.text_entry.delete('1.0', END)
        self.text_entry.grid(row=0,column=0,padx=2,pady=2)

        self.text_entry.bind('<Control-Tab>',self.text_tab)
        self.text_entry.bind('<Control-Shift-Tab>',self.text_tab)

        self.scrollbar.grid(row=0,column=1,sticky='ns')

        self.low_range_label = Label(self.range_entry_frame, text='Low Range',background=self.bg_color,fg='#ffffff')
        self.low_range_label.grid(row=0, column=0,sticky='e')
        self.low_range_entry = Entry(self.range_entry_frame, background='#DDDECE')
        self.low_range_entry.grid(row=0,column=1)

        self.hi_range_label = Label(self.range_entry_frame, text='High Range',background=self.bg_color,fg='#ffffff')
        self.hi_range_label.grid(row=1, column=0,sticky='e')
        self.hi_range_entry = Entry(self.range_entry_frame, background='#DDDECE')
        self.hi_range_entry.grid(row=1,column=1)        

    def init_footer_buttons(self):
        self.new_rule_button = Button(self.footer_frame, width=10, height=1, padx=2, pady=1, name='new button',
                                 bg=self.bg_color, fg='#ffffff', bd=1, text='New', command=self.new_rule)
        self.footer_frame.grid_columnconfigure(2, weight=0)
        self.new_rule_button.grid(row=0,column=0,sticky='w')

        self.delete_rule_button = Button(self.footer_frame, width=10, height=1, padx=2, pady=1, name='delete button',
                                 bg=self.bg_color, fg='#ffffff', bd=1, text='Delete', command=self.delete_rule)
        self.footer_frame.grid_columnconfigure(2, weight=0)
        self.delete_rule_button.grid(row=0,column=1,sticky='w')

        self.exit_button = Button(self.footer_frame, width=10, height=1, padx=2, pady=1, name='exit_button',
                                 bg=self.bg_color, fg='#ffffff', bd=1, text='Exit', command=self.exit)
        self.footer_frame.grid_columnconfigure(2, weight=1)
        self.exit_button.grid(row=0,column=3,sticky='e')

        self.save_button = Button(self.footer_frame, width=10, height=1, padx=2, pady=1, name='save button',
                                 bg=self.bg_color, fg='#ffffff', bd=1, text='Save', command=self.save_rule)
        self.footer_frame.grid_columnconfigure(2, weight=1)
        self.save_button.grid(row=0,column=4,sticky='e')

    def add_rule_button(self,rule):
        self.rule_dict[rule].rule_button = Button(self.button_frame, name='b'+rule, width=30, height=1, justify=LEFT, padx=0, pady=2,
                                                  font=('Arial', 9), fg='#ffffff',bg='#134E9C', bd=0, anchor='nw',text=rule,
                                                  highlightcolor='#134E9C',command=lambda r=rule: self.fill_text_entry_window(r))
        self.rule_dict[rule].rule_button.grid(row=self.rule_dict[rule].button_row,column=0,sticky='n')

        self.rule_dict[rule].rule_button.bind('<Tab>',self.button_tab)
        self.rule_dict[rule].rule_button.bind('<Shift-Tab>',self.button_tab)
        self.rule_dict[rule].rule_button.bind('<Return>',self.focus_text_entry)
        self.rule_dict[rule].rule_button.bind('<Right>',self.focus_text_entry)
        self.rule_dict[rule].rule_button.bind('<Up>',self.button_uparrow)
        self.rule_dict[rule].rule_button.bind('<Down>',self.button_downarrow)


    ''' init.  call the methods to build the window, set the active button and call mainloop '''
    def __init__(self):      

        self.init_window_and_frames()
        self.init_rule_widgets()
        self.init_footer_buttons()

        self.rule_dict,self.rule_index = build_rules()
        for rule in self.rule_dict:
            self.add_rule_button(rule)

        self.active_index = 0
        self.active_rule = None
        self.fill_text_entry_window(self.rule_index[self.active_index])

        self.win.mainloop()

    ''' methods to process activity from the widgets '''
    @classmethod
    def new_rule(cls):
        print('new rule - Not written yet')

    @classmethod
    def delete_rule(cls):
        print('delete rule - Not written yet')

    @classmethod
    def exit(cls):
        print('exit - Not written yet')

    @classmethod
    def save_rule(cls):
        print('save - Not written yet')

    ''' process keyboard events'''
    def button_uparrow(self,event):
        if self.active_index==0:
            next_rule=self.rule_index[len(self.rule_index)-1]
        else:
            next_rule=self.rule_index[self.active_index-1]
        self.fill_text_entry_window(next_rule)

    def button_downarrow(self,event):
        if self.active_index+1==len(self.rule_index):
            next_rule=self.rule_index[0]
        else:
            next_rule=self.rule_index[self.active_index+1]
        self.fill_text_entry_window(next_rule)

    def button_tab(self,event):
        '''
        make the next button active
        event.state == 9 means the <shift> key is pressed.  8 is shift key is not pressed
        '''
        if event.state == 9:
            if self.active_index==0:
                next_rule=self.rule_index[len(self.rule_index)-1]
            else:
                next_rule=self.rule_index[self.active_index-1]
        else:
            if self.active_index==self.active_index==len(self.rule_index)-1:
                next_rule=self.rule_index[0]
            else:
                next_rule=self.rule_index[self.active_index+1]

        self.fill_text_entry_window(next_rule)

    def focus_text_entry(self,event):
        self.text_entry_frame.focus_set()
        self.text_entry.focus()

    def text_tab(self,event):
        '''
        process control-tab, control-shift-tab key combinations from text_entry widget.  
        Attempts to set focus back to the active button
         *** doesn't work
        '''
        self.update_rule()

        self.win.focus_set()
        active_rule=self.rule_index[self.active_index]
        self.footer_frame.tkraise()
        self.range_entry_frame.tkraise()
        ''' self.button_frame_outer.tkraise() '''
        self.button_frame.focus_force()
        self.button_frame.tkraise()
        self.button_frame_outer.tkraise()
        self.button_frame.focus_force()
        self.rule_dict[active_rule].rule_button.focus_force()
        self.rule_dict[active_rule].rule_button.focus_set()
        self.rule_dict[active_rule].rule_button.focus()
        self.fill_text_entry_window(active_rule)

    def fill_text_entry_window(self,selected_rule):
        if self.active_rule is None:
            self.active_rule = selected_rule
        elif self.active_rule is not selected_rule:
            self.update_rule()

        ''' remove highlight from the old button.  Add highlight to the new one '''
        self.rule_dict[self.active_rule].rule_button.config(bg='#134E9C',fg='#ffffff')
        self.active_rule = self.rule_dict[selected_rule].rule
        self.active_index = self.rule_dict[selected_rule].button_row
        self.rule_dict[self.active_rule].rule_button.config(bg='#ffffff',fg='#134E9C')

        ''' clear the entry widgets and add the information for the new button '''
        self.low_range_entry.delete(0,END)
        self.low_range_entry.insert(0,self.rule_dict[self.active_rule].low_range)
        self.hi_range_entry.delete(0,END)
        self.text_entry.delete('1.0', END)
        self.hi_range_entry.insert(0,self.rule_dict[self.active_rule].hi_range)
        for word in self.rule_dict[self.active_rule].word_list:
            self.text_entry.insert('insert', word+'\n')

        ''' *** attempt to set the focus on the new button. '''
        self.rule_dict[self.active_rule].rule_button.focus()
        self.rule_dict[self.active_rule].rule_button.focus_set()
        self.rule_dict[self.active_rule].rule_button.focus_force()

    def update_rule(self):
        ''' save any changes made to the active rule before changing to the next one '''
        self.rule_dict[self.active_rule].word_list = sorted(self.text_entry.get(1.0,END).rstrip().split('\n'))
        self.rule_dict[self.active_rule].hi_range = decimal.Decimal(self.hi_range_entry.get())
        self.rule_dict[self.active_rule].low_range = decimal.Decimal(self.low_range_entry.get())


if __name__ == '__main__':    
    rule_entry_window()

text_tab and fill_text_entry_window are the methods I'm using to try and set the focus back on the selected button. I haven't found a combination of tkraise and focus/focus_force/focus_set that sets the focus back to the button I'd like it to be on. What can I do to force the focus back onto the button?

1 Answers

Normally when you press Control-tab in Text then tkinter runs function which moves focus to next widget on list.

After bindig function text_tab to Control-tab in Text when you press Control-tab then it sends event to your function text_tab and it changes focus to expected button but after that it sends this event also to function which normally moves focus to next widgets on list and it moves focus from your expected Button to Entry (which is next on list after `Text).

You have to use return "break" in text_tab to inform tkinter that you already used this event and it doesn't have to send/propagate event to other function(s). And this way it will no send it to function which moves focus to next widget (Entry).

 def text_tab(self, event):

     # ... code ...

     return "break"
Related