forms.SelectFromList - natural sort groups/dictionary

I added an additional argument to the class SelectFromList in the init.py file.
Also added a description and raising a PyRevitException for wrong argument input . Hope this will help someone in the future.

import re

class SelectFromList(TemplateUserInputWindow):
    """Standard form to select from a list of items.

    Any object can be passed in a list to the ``context`` argument. This class
    wraps the objects passed to context, in :obj:`TemplateListItem`.
    This class provides the necessary mechanism to make this form work both
    for selecting items from a list, and from a list of checkboxes. See the
    list of arguments below for additional options and features.

    Args:
        context (list[str] or dict[list[str]]):
            list of items to be selected from
            OR
            dict of list of items to be selected from.
            use dict when input items need to be grouped
            e.g. List of sheets grouped by sheet set.
        title (str, optional): window title. see super class for defaults.
        width (int, optional): window width. see super class for defaults.
        height (int, optional): window height. see super class for defaults.
        button_name (str, optional):
            name of select button. defaults to 'Select'
        name_attr (str, optional):
            object attribute that should be read as item name.
        multiselect (bool, optional):
            allow multi-selection (uses check boxes). defaults to False
        info_panel (bool, optional):
            show information panel and fill with .description property of item
        return_all (bool, optional):
            return all items. This is handly when some input items have states
            and the script needs to check the state changes on all items.
            This options works in multiselect mode only. defaults to False
        filterfunc (function):
            filter function to be applied to context items.
        resetfunc (function):
            reset function to be called when user clicks on Reset button
        group_selector_title (str):
            title for list group selector. defaults to 'List Group'
        default_group (str): name of defautl group to be selected
        sort_groups (str, optional): 
            Determines the sorting type applied to the list groups. This attribute can take one of the following values:
                'sorted': This will sort the groups in standard alphabetical order
                'natural': This will sort the groups in a manner that is more intuitive for human perception, especially when there are numbers involved.
                'unsorted': The groups will maintain the original order in which they were provided, without any reordering.
                Defaults to 'sorted'.


    Example:
        >>> from pyrevit import forms
        >>> items = ['item1', 'item2', 'item3']
        >>> forms.SelectFromList.show(items, button_name='Select Item')
        >>> ['item1']

        >>> from pyrevit import forms
        >>> ops = [viewsheet1, viewsheet2, viewsheet3]
        >>> res = forms.SelectFromList.show(ops,
        ...                                 multiselect=False,
        ...                                 name_attr='Name',
        ...                                 button_name='Select Sheet')

        >>> from pyrevit import forms
        >>> ops = {'Sheet Set A': [viewsheet1, viewsheet2, viewsheet3],
        ...        'Sheet Set B': [viewsheet4, viewsheet5, viewsheet6]}
        >>> res = forms.SelectFromList.show(ops,
        ...                                 multiselect=True,
        ...                                 name_attr='Name',
        ...                                 group_selector_title='Sheet Sets',
        ...                                 button_name='Select Sheets',
        ...                                 sort_groups='sorted')


        This module also provides a wrapper base class :obj:`TemplateListItem`
        for when the checkbox option is wrapping another element,
        e.g. a Revit ViewSheet. Derive from this base class and define the
        name property to customize how the checkbox is named on the dialog.

        >>> from pyrevit import forms
        >>> class MyOption(forms.TemplateListItem):
        ...    @property
        ...    def name(self):
        ...        return '{} - {}{}'.format(self.item.SheetNumber,
        ...                                  self.item.SheetNumber)
        >>> ops = [MyOption('op1'), MyOption('op2', True), MyOption('op3')]
        >>> res = forms.SelectFromList.show(ops,
        ...                                 multiselect=True,
        ...                                 button_name='Select Item')
        >>> [bool(x) for x in res]  # or [x.state for x in res]
        [True, False, True]

    """

    xaml_source = 'SelectFromList.xaml'

    @property
    def use_regex(self):
        """Is using regex?"""
        return self.regexToggle_b.IsChecked

    def _setup(self, **kwargs):
        # custom button name?
        button_name = kwargs.get('button_name', 'Select')
        if button_name:
            self.select_b.Content = button_name

        # attribute to use as name?
        self._nameattr = kwargs.get('name_attr', None)

        # multiselect?
        if kwargs.get('multiselect', False):
            self.multiselect = True
            self.list_lb.SelectionMode = Controls.SelectionMode.Extended
            self.show_element(self.checkboxbuttons_g)
        else:
            self.multiselect = False
            self.list_lb.SelectionMode = Controls.SelectionMode.Single
            self.hide_element(self.checkboxbuttons_g)

        # info panel?
        self.info_panel = kwargs.get('info_panel', False)

        # return checked items only?
        self.return_all = kwargs.get('return_all', False)

        # filter function?
        self.filter_func = kwargs.get('filterfunc', None)

        # reset function?
        self.reset_func = kwargs.get('resetfunc', None)
        if self.reset_func:
            self.show_element(self.reset_b)

        # context group title?
        self.ctx_groups_title = \
            kwargs.get('group_selector_title', 'List Group')
        self.ctx_groups_title_tb.Text = self.ctx_groups_title

        self.ctx_groups_active = kwargs.get('default_group', None)

        # group sorting?
        self.sort_groups = kwargs.get('sort_groups', 'sorted')
        if self.sort_groups not in ['sorted', 'unsorted', 'natural']:
            raise PyRevitException("Invalid value for 'sort_groups'. Allowed values are: 'sorted', 'unsorted', 'natural'.")

        # check for custom templates
        items_panel_template = kwargs.get('items_panel_template', None)
        if items_panel_template:
            self.Resources["ItemsPanelTemplate"] = items_panel_template

        item_container_template = kwargs.get('item_container_template', None)
        if item_container_template:
            self.Resources["ItemContainerTemplate"] = item_container_template

        item_template = kwargs.get('item_template', None)
        if item_template:
            self.Resources["ItemTemplate"] = \
                item_template

        # nicely wrap and prepare context for presentation, then present
        self._prepare_context()

        # setup search and filter fields
        self.hide_element(self.clrsearch_b)

        # active event listeners
        self.search_tb.TextChanged += self.search_txt_changed
        self.ctx_groups_selector_cb.SelectionChanged += self.selection_changed

        self.clear_search(None, None)

    def _prepare_context_items(self, ctx_items):
        new_ctx = []
        # filter context if necessary
        if self.filter_func:
            ctx_items = filter(self.filter_func, ctx_items)

        for item in ctx_items:
            if isinstance(item, TemplateListItem):
                item.checkable = self.multiselect
                new_ctx.append(item)
            else:
                new_ctx.append(
                    TemplateListItem(item,
                                     checkable=self.multiselect,
                                     name_attr=self._nameattr)
                    )

        return new_ctx

    @staticmethod
    def _natural_sort_key(key):
        return [int(c) if c.isdigit() else c.lower() for c in re.split('(\d+)', key)]

    def _prepare_context(self):
        if isinstance(self._context, dict) and self._context.keys():
            # Sort the groups if necessary
            if self.sort_groups == "sorted":
                sorted_groups = sorted(self._context.keys())
            elif self.sort_groups == "natural":
                sorted_groups = sorted(self._context.keys(), key=self._natural_sort_key)
            else:
                sorted_groups = self._context.keys()  # No sorting
            
            self._update_ctx_groups(sorted_groups)
            
            new_ctx = OrderedDict()
            for ctx_grp in sorted_groups:
                items = self._prepare_context_items(self._context[ctx_grp])
                new_ctx[ctx_grp] = items  # Do not sort the items within the groups

            self._context = new_ctx
        else:
            self._context = self._prepare_context_items(self._context)