Beispiel #1
0
class CrossSelect(SelectMultiple):
    """
    CrossSelect provides a two-tab multi-selection widget with regex
    text filtering. Items can be transferred with buttons between the
    selected and unselected options.
    """
    def __init__(self, *args, **kwargs):
        # Compute selected and unselected values
        options = kwargs.get('options', {})
        if isinstance(options, list):
            options = named_objs([(opt, opt) for opt in options])
        self._reverse_lookup = {v: k for k, v in options.items()}
        selected = [self._reverse_lookup[v] for v in kwargs.get('value', [])]
        unselected = [k for k in options if k not in selected]

        # Define whitelist and blacklist
        self._lists = {
            False: SelectMultiple(options=unselected),
            True: SelectMultiple(options=selected)
        }

        self._lists[False].observe(self._update_selection, 'value')
        self._lists[True].observe(self._update_selection, 'value')

        # Define buttons
        button_layout = Layout(width='50px')
        self._buttons = {
            False: Button(description='<<', layout=button_layout),
            True: Button(description='>>', layout=button_layout)
        }
        self._buttons[False].on_click(self._apply_selection)
        self._buttons[True].on_click(self._apply_selection)

        # Define search
        self._search = {
            False: Text(placeholder='Filter available options'),
            True: Text(placeholder='Filter selected options')
        }
        self._search[False].observe(self._filter_options, 'value')
        self._search[True].observe(self._filter_options, 'value')

        # Define Layout
        no_margin = Layout(margin='0')
        row_layout = Layout(margin='0',
                            display='flex',
                            justify_content='space-between')

        search_row = HBox([self._search[False], self._search[True]])
        search_row.layout = row_layout
        button_box = VBox([self._buttons[True], self._buttons[False]],
                          layout=Layout(margin='auto 0'))
        tab_row = HBox([self._lists[False], button_box, self._lists[True]])
        tab_row.layout = row_layout
        self._composite = VBox([search_row, tab_row], layout=no_margin)

        self.observe(self._update_options, 'options')
        self.observe(self._update_value, 'value')

        self._selected = {False: [], True: []}
        self._query = {False: '', True: ''}
        super(CrossSelect, self).__init__(*args, **dict(kwargs,
                                                        options=options))

    def _update_value(self, event):
        selected = [self._reverse_lookup.get(v, v) for v in event['new']]
        self._lists[True].options = selected
        self._lists[True].value = []
        self._lists[False].options = [
            o for o in self.options if o not in selected
        ]

    def _update_options(self, event):
        """
        Updates the options of each of the sublists after the options
        for the whole widget are updated.
        """
        self._reverse_lookup = {v: k for k, v in event['new'].items()}
        options = list(event['new'].keys()) if isinstance(
            event, dict) else event['new']
        self._selected[False] = []
        self._selected[True] = []
        self._lists[True].options = ['']
        self._lists[True].value = []
        self._lists[False].options = options
        self._lists[False].value = []
        self._apply_filters()

    def _apply_filters(self):
        self._filter_options({'owner': self._search[False]})
        self._filter_options({'owner': self._search[True]})

    def _filter_options(self, event):
        """
        Filters unselected options based on a text query event.
        """
        selected = event['owner'] is self._search[True]
        query = self._query[selected] if 'new' not in event else event['new']
        self._query[selected] = query
        other = self._lists[not selected].options
        options = [o for o in self.options if o not in other]
        if not query:
            self._lists[selected].options = options
            self._lists[selected].value = []
        else:
            try:
                match = re.compile(query)
                matches = list(filter(match.search, options))
                options = matches + [
                    opt for opt in options if opt not in matches
                ]
            except:
                matches = options
            self._lists[selected].options = options if options else ['']
            self._lists[selected].value = matches

    def _update_selection(self, event):
        """
        Updates the current selection in each list.
        """
        selected = event['owner'] is self._lists[True]
        self._selected[selected] = [v for v in event['new'] if v != '']

    def _apply_selection(self, event):
        """
        Applies the current selection depending on which button was
        pressed.
        """
        selected = event is self._buttons[True]
        new = self._selected[not selected]
        old = self._lists[selected].options
        other = self._lists[not selected].options

        merged = sorted([v for v in list(old) + list(new) if v != ''])
        leftovers = sorted([o for o in other if o not in new and o != ''])
        new_values = merged if selected else leftovers
        self._lists[selected].options = merged if merged else ['']
        self._lists[not selected].options = leftovers if leftovers else ['']
        self.value = [
            self._options_dict[o] for o in self._lists[True].options if o != ''
        ]
        self._apply_filters()

    def _ipython_display_(self, **kwargs):
        """
        Displays the composite widget.
        """
        self._composite._ipython_display_(**kwargs)

    def get_state(self, key=None, drop_defaults=False):
        # HACK: Lets this composite widget pretend to be a regular widget
        # when included into a layout.
        if key in ['value', '_options_labels']:
            return super(CrossSelect, self).get_state(key)
        return self._composite.get_state(key)