Text Control Box Focus Traversal Between Frame and Notebook Tabs

I am using Python 3.7.5 and wxPython 4.0.4.

I have a GUI that has a Frame which contains a notebook with three tabs and a pair of Text Control boxes used for common functionality for all notebook tabs and several Text Control boxes on two of the notebook tabs. The Text Control boxes on those tabs control functionality solely related to those tabs.

The implemented functionality is:

  1. Text Control Box traversal responds to the Enter key and matches standard traversal behavior. Enter moves to next Text Control box, and wrapping occurs from the bottom to the top Text Control box of the active tab. This requires switching the Text Control box focus back and forth between the main Frame and the notebook. I specifically want the Enter key to cause the Text Control box traversal as I think it is more natural than the TAB key.

  2. Shift+Enter in any Text Control Box will invoke the plot function.

I’m wondering about what is the best way to handle traversal between Text Control boxes split between a Frame and a notebook tab. The attached code works correctly and is based on catching the active notebook tab and Text Control box and setting the new focus with wx.CallAfter when the Enter key is pressed.

I have two questions:

  1. Is it bad practice to bind the notebook Text Control boxes in the main Frame?
  2. Is using wx.CallAfter the correct way to get the desired Text Control box traversal?

Any feedback on my approach or improvements would be welcomed.

import wx


class MainFrame(wx.Frame):
    '''
    GUI main frame.
    '''

    def __init__(self, parent, title='Test', size=(300, 300), pos=(300, 300)):

        super().__init__(None, wx.ID_ANY, title=title, size=size, pos=pos)

        # Top level panel.
        self.panel = wx.Panel(self)

        # Define notebook.
        self.notebook = wx.Notebook(self.panel)
        self.tabPlot = PlotPanel(self.notebook, 'Plot')
        self.notebook.AddPage(self.tabPlot, 'Plot')
        self.tabPrint = PrintPanel(self.notebook, 'Print')
        self.notebook.AddPage(self.tabPrint, 'Print')
        self.tabData = DataPanel(self.notebook, 'Data')
        self.notebook.AddPage(self.tabData, 'Data')

        # Define main frame headers and text control boxes.
        self.x_text_header = wx.StaticText(self.panel, label='FilterX')
        self.x_text_box = wx.TextCtrl(self.panel, wx.ID_ANY, '',
                                      size=(100, -1),
                                      name='FilterX',
                                      style=wx.TE_PROCESS_ENTER)

        self.y_text_header = wx.StaticText(self.panel, label='FilterY')
        self.y_text_box = wx.TextCtrl(self.panel, wx.ID_ANY, '',
                                      size=(100, -1),
                                      name='FilterY',
                                      style=wx.TE_PROCESS_ENTER)

        # Do main frame layout.
        main_vertSizer = wx.BoxSizer(wx.VERTICAL)
        main_vertSizer.Add(self.notebook, proportion=1, flag=wx.EXPAND)
        main_vertSizer.Add((0, 10))
        main_vertSizer.Add(self.x_text_header, proportion=0)
        main_vertSizer.Add((0, 10))
        main_vertSizer.Add(self.x_text_box, proportion=0)
        main_vertSizer.Add((0, 10))
        main_vertSizer.Add(self.y_text_header, proportion=0)
        main_vertSizer.Add((0, 10))
        main_vertSizer.Add(self.y_text_box, proportion=0)
        main_vertSizer.Add((0, 10))
        self.panel.SetSizer(main_vertSizer)
        self.Layout()
        self.Show()

        # Bind widgets.
        self.x_text_box.Bind(wx.EVT_TEXT_ENTER, self.onTxtEnter)
        self.y_text_box.Bind(wx.EVT_TEXT_ENTER, self.onTxtEnter)
        self.tabPlot.plot_title_text.Bind(wx.EVT_TEXT_ENTER, self.onTxtEnter)
        self.tabPlot.plot_control_text.Bind(wx.EVT_TEXT_ENTER, self.onTxtEnter)
        self.tabPrint.print_control_text.Bind(wx.EVT_TEXT_ENTER,
                                              self.onTxtEnter)

        self.x_text_box.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
        self.y_text_box.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
        self.tabPlot.plot_title_text.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
        self.tabPlot.plot_control_text.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
        self.tabPrint.print_control_text.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)

        self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)

        # Set initial focus and page.
        wx.CallAfter(self.tabPlot.plot_control_text.SetFocus)
        self.current_page_name = 'Plot'

    def OnPageChanged(self, event):
        '''Get current notebook page and set focus.
        '''

        # Get current page name.
        self.current_page_name = \
            self.notebook.GetPage(event.GetSelection()).name

        # Set focus for each panel.
        if self.current_page_name == 'Plot':
            wx.CallAfter(self.tabPlot.plot_control_text.SetFocus)
        elif self.current_page_name == 'Print':
            wx.CallAfter(self.tabPrint.print_control_text.SetFocus)
        else:
            wx.CallAfter(self.x_text_box.SetFocus)

    def onTxtEnter(self, event):
        '''Call the appropriate helper function and move the text box focus.
        '''
        textbox_name = event.GetEventObject().GetName()

        # Always traverse to filter y text box.
        if textbox_name == 'FilterX':
            self.do_filter_x()
            wx.CallAfter(self.y_text_box.SetFocus)

        # Traverse back to first text box in notebook tab if possible,
        # otherwise back to the x filter text box.
        if textbox_name == 'FilterY':
            self.do_filter_y()
            if self.current_page_name == 'Plot':
                wx.CallAfter(self.tabPlot.plot_title_text.SetFocus)
            elif self.current_page_name == 'Print':
                wx.CallAfter(self.tabPrint.print_control_text.SetFocus)
            else:
                wx.CallAfter(self.x_text_box.SetFocus)

        # Always traverse to plot control text box.
        if textbox_name == 'PlotTitle':
            self.do_plot_title()
            wx.CallAfter(self.tabPlot.plot_control_text.SetFocus)

        # Always traverse to filter x text box.
        if textbox_name == 'PlotControl':
            self.do_plot_control()
            wx.CallAfter(self.x_text_box.SetFocus)

        # Always traverse to filter x text box.
        if textbox_name == 'PrintControl':
            self.do_print_control()
            wx.CallAfter(self.x_text_box.SetFocus)

    def onKeyDown(self, event):
        '''
        Call the plot routine if shift+ENTER is pressed.
        '''
        key = event.GetKeyCode()
        shift = event.ShiftDown()

        if shift and key == wx.WXK_RETURN:
            self.plot_data()
        else:
            event.Skip()

    # Stub out helper functions.
    def do_filter_x(self):
        print('Call Update filter x function.')

    def do_filter_y(self):
        print('Call Update filter y function.')

    def do_plot_title(self):
        print('Call update plot title function.')

    def do_plot_control(self):
        print('Call plot control function.')

    def do_print_control(self):
        print('Call print control function.')

    def plot_data(self):
        print('Plotting Data')


class PlotPanel(wx.Panel):
    '''
    The plot panel class to derive the plot tab of the notebook.
    '''

    def __init__(self, parent, name):
        ''''''
        super().__init__(parent=parent)

        # Panel name.
        self.name = name

        # Plot title string text control box.
        self.plot_title_header = wx.StaticText(self, wx.ID_ANY,
                                               label='PlotTitle',
                                               pos=(0, 0))

        self.plot_title_text = wx.TextCtrl(self, wx.ID_ANY, "",
                                           size=(100, -1),
                                           style=wx.TE_PROCESS_ENTER,
                                           name='PlotTitle',
                                           pos=(0, 20))

        # Plot title string text control box.
        self.plot_control_header = wx.StaticText(self, wx.ID_ANY,
                                                 label='PlotControl',
                                                 pos=(0, 50))

        # Plot control string text control box.
        self.plot_control_text = wx.TextCtrl(self, wx.ID_ANY, "",
                                             size=(100, -1),
                                             style=wx.TE_PROCESS_ENTER,
                                             name='PlotControl',
                                             pos=(0, 70))
        # More stuff ....


class PrintPanel(wx.Panel):
    '''
    The output panel class to derive the output tab of the notebook.
    '''

    def __init__(self, parent, name):
        ''''''
        super().__init__(parent=parent)

        # Panel name.
        self.name = name

        self.print_control_header = wx.StaticText(self, wx.ID_ANY,
                                                  label='PrintControl',
                                                  pos=(0, 0))

        self.print_control_text = wx.TextCtrl(self, wx.ID_ANY, "",
                                              size=(100, -1),
                                              style=wx.TE_PROCESS_ENTER,
                                              name='PrintControl',
                                              pos=(0, 20))
        # More stuff ....


class DataPanel(wx.Panel):
    '''
    The data panel class to derive the data tab of the notebook.
    '''

    def __init__(self, parent, name):
        ''''''
        super().__init__(parent=parent)

        # Panel name.
        self.name = name

        # More stuff ....


if __name__ == "__main__":
    app = wx.App(False)
    MainFrame(None, size=(200, 300), pos=(600, 100))
    app.MainLoop()

I usually say that if it works, don’t fix it. Using wx.CallAfter is fine, however…

wx.CallAfter(self.tabPlot.plot_control_text.SetFocus)

…you’ll probably want to be careful about things like that where where there are many.levels.of.attribute.access() :wink: because it’s easy to get into a situation where slight changes in layout or other organizational changes can have big ripple effects.

Thanks Robin for the feedback; I’m glad that using the wx.CallAfter is okay as it seemed like the only way to control the text box focus.

I agree that my hierarchy certainly may lead to lots of editing if I have to reorganize my GUI layout; its just that I haven’t figured out a better way of handling the need to traverse from text control boxes that are common to all notebook tabs and those that are unique to the individual tabs.