コード例 #1
0
def handle_option_merge(group_defaults, incoming_options, title):
    """
    Merges a set of group defaults with incoming options.

    A bunch of ceremony here is to ensure backwards compatibility with the old
    num_required_cols and num_optional_cols decorator args. They are used as
    the seed values for the new group defaults which keeps the old behavior
    _mostly_ in tact.

    Known failure points:
        * Using custom groups / names. No 'positional arguments' group
          means no required_cols arg being honored
        * Non-positional args marked as required. It would take group
          shuffling along the lines of that required to make
          mutually exclusive groups show in the correct place. In short, not
          worth the complexity for a legacy feature that's been succeeded by
          a much more powerful alternative.
    """
    if title == 'positional arguments':
        # the argparse default 'required' bucket
        req_cols = getin(group_defaults, ['legacy', 'required_cols'], 2)
        new_defaults = assoc(group_defaults, 'columns', req_cols)
        return merge(new_defaults, incoming_options)
    else:
        opt_cols = getin(group_defaults, ['legacy', 'optional_cols'], 2)
        new_defaults = assoc(group_defaults, 'columns', opt_cols)
        return merge(new_defaults, incoming_options)
コード例 #2
0
ファイル: bases.py プロジェクト: xinqinglee/Gooey
    def getValue(self):
        regexFunc = lambda x: bool(re.match(userValidator, x))

        userValidator = getin(self._options, ['validator', 'test'], 'True')
        message = getin(self._options, ['validator', 'message'], '')
        testFunc = regexFunc \
                   if getin(self._options, ['validator', 'type'], None) == 'RegexValidator'\
                   else eval('lambda user_input: bool(%s)' % userValidator)
        satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
        value = self.getWidgetValue()

        return {
            'id':
            self._id,
            'cmd':
            self.formatOutput(self._meta, value),
            'rawValue':
            value,
            'test':
            runValidator(satisfies, value),
            'error':
            None if runValidator(satisfies, value) else message,
            'clitype':
            'positional' if self._meta['required']
            and not self._meta['commands'] else 'optional'
        }
コード例 #3
0
ファイル: bases.py プロジェクト: Magnati/Gooey
    def getValue(self):
        userValidatorCB = None
        validatesCB = True
        value = self.getWidgetValue()
        if 'callback' in self._options['validator']:
            userValidatorCB = self._options['validator']['callback']
            validatesCB = userValidatorCB(value)

        userValidator = getin(self._options, ['validator', 'test'], 'True')
        message = getin(self._options, ['validator', 'message'], '')
        testFunc = eval('lambda user_input: bool(%s)' % userValidator)
        satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
        return {
            'id':
            self._id,
            'cmd':
            self.formatOutput(self._meta, value),
            'rawValue':
            value,
            'test':
            runValidator(satisfies, value),
            'error':
            None
            if runValidator(satisfies, value) and validatesCB else message,
            'clitype':
            'positional' if self._meta['required']
            and not self._meta['commands'] else 'optional'
        }
コード例 #4
0
    def makeGroup(self, parent, thissizer, group, *args):
        '''
        Messily builds the (potentially) nested and grouped layout

        Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
        instantiated! I cannot figure out how to split out the creation of the
        widgets from their styling without WxPython violently exploding

        TODO: sort out the WX quirks and clean this up.
        '''

        # determine the type of border , if any, the main sizer will use
        if getin(group, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(parent, -1, group['name'] or '')
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            if group['name']:
                boxSizer.Add(wx_util.h1(parent, group['name'] or ''), 0,
                             wx.TOP | wx.BOTTOM | wx.LEFT, 8)

        group_description = getin(group, ['description'])
        if group_description:
            description = wx.StaticText(parent, label=group_description)
            boxSizer.Add(description, 0, wx.EXPAND | wx.LEFT, 10)

        # apply an underline when a grouping border is not specified
        if not getin(group, ['options', 'show_border'],
                     False) and group['name']:
            boxSizer.Add(wx_util.horizontal_rule(parent), 0,
                         wx.EXPAND | wx.LEFT, 10)

        ui_groups = self.chunkWidgets(group)

        for uigroup in ui_groups:
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            for item in uigroup:
                widget = self.reifyWidget(parent, item)
                # !Mutate the reifiedWidgets instance variable in place
                self.reifiedWidgets.append(widget)
                sizer.Add(widget, 1, wx.ALL, 5)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)

        # apply the same layout rules recursively for subgroups
        hs = wx.BoxSizer(wx.HORIZONTAL)
        for e, subgroup in enumerate(group['groups']):
            self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
            if e % getin(group, ['options', 'columns'], 2) \
                    or e == len(group['groups']):
                boxSizer.Add(hs, *args)
                hs = wx.BoxSizer(wx.HORIZONTAL)

        thissizer.Add(boxSizer, *args)
コード例 #5
0
 def test_choice_string_cooersion(self):
     """
     Issue 321 - must coerce choice types to string to support wx.ComboBox
     """
     parser = ArgumentParser()
     parser.add_argument('--foo', default=1, choices=[1, 2, 3])
     choice_action = parser._actions[-1]
     result = argparse_to_json.action_to_json(choice_action, 'Dropdown', {})
     self.assertEqual(getin(result, ['data', 'choices']), ['1', '2', '3'])
     # default value is also converted to a string type
     self.assertEqual(getin(result, ['data', 'default']), '1')
コード例 #6
0
ファイル: config.py プロジェクト: arunkgupta/Gooey
    def makeGroup(self, parent, thissizer, group, *args):
        '''
        Messily builds the (potentially) nested and grouped layout

        Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
        instantiated! I cannot figure out how to split out the creation of the
        widgets from their styling without WxPython violently exploding

        TODO: sort out the WX quirks and clean this up.
        '''

        # determine the type of border , if any, the main sizer will use
        if getin(group, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(parent, -1, group['name'] or '')
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            if group['name']:
                boxSizer.Add(wx_util.h1(parent, group['name'] or ''), 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)

        group_description = getin(group, ['description'])
        if group_description:
            description = wx.StaticText(parent, label=group_description)
            boxSizer.Add(description, 0,  wx.EXPAND | wx.LEFT, 10)

        # apply an underline when a grouping border is not specified
        if not getin(group, ['options', 'show_border'], False) and group['name']:
            boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)

        ui_groups = self.chunkWidgets(group)

        for uigroup in ui_groups:
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            for item in uigroup:
                widget = self.reifyWidget(parent, item)
                # !Mutate the reifiedWidgets instance variable in place
                self.reifiedWidgets.append(widget)
                sizer.Add(widget, 1, wx.ALL, 5)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)

        # apply the same layout rules recursively for subgroups
        hs = wx.BoxSizer(wx.HORIZONTAL)
        for e, subgroup in enumerate(group['groups']):
            self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
            if e % getin(group, ['options', 'columns'], 2) \
                    or e == len(group['groups']):
                boxSizer.Add(hs, *args)
                hs = wx.BoxSizer(wx.HORIZONTAL)

        thissizer.Add(boxSizer, *args)
コード例 #7
0
    def test_suppress_is_removed_as_default_value(self):
        """
        Issue #469
        Argparse uses the literal string ==SUPPRESS== as an internal flag.
        When encountered in Gooey, these should be dropped and mapped to `None`.
        """
        parser = ArgumentParser(prog='test_program')
        parser.add_argument("--foo", default=argparse.SUPPRESS)
        parser.add_argument('--version', action='version', version='1.0')

        result = argparse_to_json.convert(parser, required_cols=2, optional_cols=2)
        groups = getin(result, ['widgets', 'test_program', 'contents'])
        for item in groups[0]['items']:
            self.assertEqual(getin(item, ['data', 'default']), None)
コード例 #8
0
 def createWidgets(self):
     """
     Instantiate the Gooey Widgets that are used within the RadioGroup
     """
     from gooey.gui.components import widgets
     return [getattr(widgets, item['type'])(self, item)
             for item in getin(self.widgetInfo, ['data', 'widgets'], [])]
コード例 #9
0
 def test_version_maps_to_checkbox(self):
     testcases = [
         [['--version'], {}, 'TextField'],
         # we only remap if the action is version
         # i.e. we don't care about the argument name itself
         [['--version'], {
             'action': 'store'
         }, 'TextField'],
         # should get mapped to CheckBox becuase of the action
         [['--version'], {
             'action': 'version'
         }, 'CheckBox'],
         # ditto, even through the 'name' isn't 'version'
         [['--foobar'], {
             'action': 'version'
         }, 'CheckBox'],
     ]
     for args, kwargs, expectedType in testcases:
         with self.subTest([args, kwargs]):
             parser = argparse.ArgumentParser(prog='test')
             parser.add_argument(*args, **kwargs)
             result = argparse_to_json.convert(parser,
                                               num_required_cols=2,
                                               num_optional_cols=2)
             contents = getin(result, ['widgets', 'test', 'contents'])[0]
             self.assertEqual(contents['items'][0]['type'], expectedType)
コード例 #10
0
ファイル: config.py プロジェクト: arunkgupta/Gooey
 def chunkWidgets(self, group):
     ''' chunk the widgets up into groups based on their sizing hints '''
     ui_groups = []
     subgroup = []
     for index, item in enumerate(group['items']):
         if getin(item, ['options', 'full_width'], False):
             ui_groups.append(subgroup)
             ui_groups.append([item])
             subgroup = []
         else:
             subgroup.append(item)
         if len(subgroup) == getin(group, ['options', 'columns'], 2) \
                 or item == group['items'][-1]:
             ui_groups.append(subgroup)
             subgroup = []
     return ui_groups
コード例 #11
0
 def chunkWidgets(self, group):
     ''' chunk the widgets up into groups based on their sizing hints '''
     ui_groups = []
     subgroup = []
     for index, item in enumerate(group['items']):
         if getin(item, ['options', 'full_width'], False):
             ui_groups.append(subgroup)
             ui_groups.append([item])
             subgroup = []
         else:
             subgroup.append(item)
         if len(subgroup) == getin(group, ['options', 'columns'], 2) \
                 or item == group['items'][-1]:
             ui_groups.append(subgroup)
             subgroup = []
     return ui_groups
コード例 #12
0
    def arrange(self, *args, **kwargs):
        title = getin(self.widgetInfo, ['options', 'title'], 'Choose One')
        if getin(self.widgetInfo, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(self, -1, title)
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            boxSizer.Add(wx_util.h1(self, title), 0)

        for btn, widget in zip(self.radioButtons, self.widgets):
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(btn,0, wx.RIGHT, 4)
            sizer.Add(widget, 1, wx.EXPAND)
            boxSizer.Add(sizer, 1, wx.ALL | wx.EXPAND, 5)
        self.SetSizer(boxSizer)
コード例 #13
0
ファイル: radio_group.py プロジェクト: chriskiehl/Gooey
    def arrange(self, *args, **kwargs):
        title = getin(self.widgetInfo, ['options', 'title'], _('choose_one'))
        if getin(self.widgetInfo, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(self, -1, title)
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            boxSizer.Add(wx_util.h1(self, title), 0)

        for btn, widget in zip(self.radioButtons, self.widgets):
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(btn,0, wx.RIGHT, 4)
            sizer.Add(widget, 1, wx.EXPAND)
            boxSizer.Add(sizer, 1, wx.ALL | wx.EXPAND, 5)
        self.SetSizer(boxSizer)
コード例 #14
0
ファイル: radio_group.py プロジェクト: chriskiehl/Gooey
 def createWidgets(self):
     """
     Instantiate the Gooey Widgets that are used within the RadioGroup
     """
     from gooey.gui.components import widgets
     return [getattr(widgets, item['type'])(self, item)
             for item in getin(self.widgetInfo, ['data', 'widgets'], [])]
コード例 #15
0
ファイル: bases.py プロジェクト: arunkgupta/Gooey
    def getValue(self):
        userValidator = getin(self._options, ['validator', 'test'], 'True')
        message = getin(self._options, ['validator', 'message'], '')
        testFunc = eval('lambda user_input: bool(%s)' % userValidator)
        satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
        value = self.getWidgetValue()

        return {
            'id': self._id,
            'cmd': self.formatOutput(self._meta, value),
            'rawValue': value,
            'test': runValidator(satisfies, value),
            'error': None if runValidator(satisfies, value) else message,
            'clitype': 'positional'
                        if self._meta['required'] and not self._meta['commands']
                        else 'optional'
        }
コード例 #16
0
    def arrange(self, *args, **kwargs):
        title = getin(self.widgetInfo, ['options', 'title'], _('choose_one'))
        if getin(self.widgetInfo, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(self, -1, title)
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            title = wx_util.h1(self, title)
            title.SetForegroundColour(self._options['label_color'])
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            boxSizer.Add(title, 0)

        for btn, widget in zip(self.radioButtons, self.widgets):
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(btn,0, wx.RIGHT, 4)
            sizer.Add(widget, 1, wx.EXPAND)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
        self.SetSizer(boxSizer)
コード例 #17
0
    def test_choice_string_cooersion_no_default(self):
        """
        Make sure that choice types without a default don't create
        the literal string "None" but stick with the value None
        """
        parser = ArgumentParser()
        parser.add_argument('--foo', choices=[1, 2, 3])

        choice_action = parser._actions[-1]
        result = argparse_to_json.action_to_json(choice_action, 'Dropdown', {})
        self.assertEqual(getin(result, ['data', 'default']), None)
コード例 #18
0
def apply_default_rewrites(spec):
    top_level_subgroups = list(spec['widgets'].keys())

    for subgroup in top_level_subgroups:
        path = ['widgets', subgroup, 'contents']
        contents = getin(spec, path)
        for group in contents:
            if group['name'] == 'positional arguments':
                group['name'] = 'required_args_msg'
            if group['name'] == 'optional arguments':
                group['name'] = 'optional_args_msg'
    return spec
コード例 #19
0
 def createWidgets(self):
     """
     Instantiate the Gooey Widgets that are used within the RadioGroup
     """
     from gooey.gui.components import widgets
     widgets = [getattr(widgets, item['type'])(self, item)
                for item in getin(self.widgetInfo, ['data', 'widgets'], [])]
     # widgets should be disabled unless
     # explicitly selected
     for widget in widgets:
         widget.Disable()
     return widgets
コード例 #20
0
    def test_listbox_defaults_cast_correctly(self):
        """
        Issue XXX - defaults supplied in a list were turned into a string
        wholesale (list and all). The defaults should be stored as a list
        proper with only the _internal_ values coerced to strings.
        """
        parser = GooeyParser()
        parser.add_argument('--foo', widget="Listbox", nargs="*", choices=[1, 2, 3], default=[1, 2])

        choice_action = parser._actions[-1]
        result = argparse_to_json.action_to_json(choice_action, 'Listbox', {})
        self.assertEqual(getin(result, ['data', 'default']), ['1', '2'])
コード例 #21
0
    def test_listbox_single_default_cast_correctly(self):
        """
        Single arg defaults to listbox should be wrapped in a list and
        their contents coerced as usual.
        """
        parser = GooeyParser()
        parser.add_argument('--foo', widget="Listbox",
                            nargs="*", choices=[1, 2, 3], default="sup")

        choice_action = parser._actions[-1]
        result = argparse_to_json.action_to_json(choice_action, 'Listbox', {})
        self.assertEqual(getin(result, ['data', 'default']), ['sup'])
コード例 #22
0
    def createRadioButtons(self):
        # button groups in wx are statefully determined via a style flag
        # on the first button (what???). All button instances are part of the
        # same group until a new button is created with the style flag RG_GROUP
        # https://wxpython.org/Phoenix/docs/html/wx.RadioButton.html
        # (What???)
        firstButton = wx.RadioButton(self, style=wx.RB_GROUP)
        firstButton.SetValue(False)
        buttons = [firstButton]

        for _ in getin(self.widgetInfo, ['data','widgets'], [])[1:]:
            buttons.append(wx.RadioButton(self))
        return buttons
コード例 #23
0
ファイル: radio_group.py プロジェクト: chriskiehl/Gooey
    def createRadioButtons(self):
        # button groups in wx are statefully determined via a style flag
        # on the first button (what???). All button instances are part of the
        # same group until a new button is created with the style flag RG_GROUP
        # https://wxpython.org/Phoenix/docs/html/wx.RadioButton.html
        # (What???)
        firstButton = wx.RadioButton(self, style=wx.RB_GROUP)
        firstButton.SetValue(False)
        buttons = [firstButton]

        for _ in getin(self.widgetInfo, ['data','widgets'], [])[1:]:
            buttons.append(wx.RadioButton(self))
        return buttons
コード例 #24
0
ファイル: bases.py プロジェクト: fakegit/Gooey
    def getValue(self) -> t.FieldValue:
        regexFunc: Callable[[str],
                            bool] = lambda x: bool(re.match(userValidator, x))

        userValidator = getin(self._options, ['validator', 'test'], 'True')
        message = getin(self._options, ['validator', 'message'], '')
        testFunc = regexFunc \
                   if getin(self._options, ['validator', 'type'], None) == 'RegexValidator'\
                   else eval('lambda user_input: bool(%s)' % userValidator)
        satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
        value = self.getWidgetValue()

        return t.FieldValue(  # type: ignore
            id=self._id,
            cmd=self.formatOutput(self._meta, value),
            meta=self._meta,
            rawValue=value,
            # type=self.info['type'],
            enabled=self.IsEnabled(),
            visible=self.IsShown(),
            test=runValidator(satisfies, value),
            error=None if runValidator(satisfies, value) else message,
            clitype=('positional' if self._meta['required']
                     and not self._meta['commands'] else 'optional'))
コード例 #25
0
    def __init__(self, parent, widgetInfo, *args, **kwargs):
        super(RadioGroup, self).__init__(parent, *args, **kwargs)
        self._parent = parent
        self.info = widgetInfo
        self._id = widgetInfo['id']
        self.widgetInfo = widgetInfo
        self.error = wx.StaticText(self, label='')
        self.radioButtons = self.createRadioButtons()
        self.selected = None
        self.widgets = self.createWidgets()
        self.arrange()
        self.applyStyleRules()

        for button in self.radioButtons:
            button.Bind(wx.EVT_LEFT_DOWN, self.handleButtonClick)

        initialSelection = getin(self.info, ['options', 'initial_selection'], None)
        if initialSelection is not None:
            self.selected = self.radioButtons[initialSelection]
            self.selected.SetValue(True)
        self.handleImplicitCheck()
コード例 #26
0
ファイル: radio_group.py プロジェクト: chriskiehl/Gooey
    def __init__(self, parent, widgetInfo, *args, **kwargs):
        super(RadioGroup, self).__init__(parent, *args, **kwargs)
        self._parent = parent
        self.info = widgetInfo
        self._id = widgetInfo['id']
        self.widgetInfo = widgetInfo
        self.error = wx.StaticText(self, label='')
        self.radioButtons = self.createRadioButtons()
        self.selected = None
        self.widgets = self.createWidgets()
        self.arrange()
        self.applyStyleRules()

        for button in self.radioButtons:
            button.Bind(wx.EVT_LEFT_DOWN, self.handleButtonClick)

        initialSelection = getin(self.info, ['options', 'initial_selection'], None)
        if initialSelection is not None:
            self.selected = self.radioButtons[initialSelection]
            self.selected.SetValue(True)
        self.handleImplicitCheck()
コード例 #27
0
    def makeGroup(self, parent, thissizer, group, *args):
        '''
        Messily builds the (potentially) nested and grouped layout

        Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
        instantiated! I cannot figure out how to split out the creation of the
        widgets from their styling without WxPython violently exploding

        TODO: sort out the WX quirks and clean this up.
        '''

        # determine the type of border , if any, the main sizer will use
        if getin(group, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(parent, -1, self.getName(group) or '')
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            if group['name']:
                groupName = wx_util.h1(parent, self.getName(group) or '')
                groupName.SetForegroundColour(getin(group, ['options', 'label_color']))
                groupName.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
                boxSizer.Add(groupName, 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)

        group_description = getin(group, ['description'])
        if group_description:
            description = AutoWrappedStaticText(parent, label=group_description, target=boxSizer)
            description.SetForegroundColour(getin(group, ['options', 'description_color']))
            description.SetMinSize((0, -1))
            description.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
            boxSizer.Add(description, 1,  wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)

        # apply an underline when a grouping border is not specified
        # unless the user specifically requests not to show it
        if not getin(group, ['options', 'show_border'], False) and group['name'] \
                and getin(group, ['options', 'show_underline'], True):
            boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)

        ui_groups = self.chunkWidgets(group)

        for uigroup in ui_groups:
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            for item in uigroup:
                widget = self.reifyWidget(parent, item)
                if not getin(item, ['options', 'visible'], True):
                    widget.Hide()
                # !Mutate the reifiedWidgets instance variable in place
                self.reifiedWidgets.append(widget)
                sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 5)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)

        # apply the same layout rules recursively for subgroups
        hs = wx.BoxSizer(wx.HORIZONTAL)
        for e, subgroup in enumerate(group['groups']):
            self.makeGroup(parent, hs, subgroup, 1, wx.EXPAND)
            if len(group['groups']) != e:
                hs.AddSpacer(5)

            # self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
            itemsPerColumn = getin(group, ['options', 'columns'], 2)
            if e % itemsPerColumn or (e + 1) == len(group['groups']):
                boxSizer.Add(hs, *args)
                hs = wx.BoxSizer(wx.HORIZONTAL)


        group_top_margin = getin(group, ['options', 'margin_top'], 1)

        marginSizer = wx.BoxSizer(wx.VERTICAL)
        marginSizer.Add(boxSizer, 1, wx.EXPAND | wx.TOP, group_top_margin)

        thissizer.Add(marginSizer, *args)