def test_index_trigger(self):
     select = Select(options=[1, 2, 3])
     observations = []
     def f(change):
         observations.append(change.new)
     select.observe(f, 'index')
     assert select.index == 0
     select.options = [4, 5, 6]
     assert select.index == 0
     assert select.value == 4
     assert select.label == '4'
     assert observations == [0]
Ejemplo n.º 2
0
class Dashboard(VBox):
    """
    Build the dashboard for Jupyter widgets. Requires running
    in a notebook/jupyterlab.
    """
    def __init__(self, net, width="95%", height="550px", play_rate=0.5):
        self._ignore_layer_updates = False
        self.player = _Player(self, play_rate)
        self.player.start()
        self.net = net
        r = random.randint(1, 1000000)
        self.class_id = "picture-dashboard-%s-%s" % (self.net.name, r)
        self._width = width
        self._height = height
        ## Global widgets:
        style = {"description_width": "initial"}
        self.feature_columns = IntText(description="Detail columns:",
                                       value=self.net.config["dashboard.features.columns"],
                                       min=0,
                                       max=1024,
                                       style=style)
        self.feature_scale = FloatText(description="Detail scale:",
                                       value=self.net.config["dashboard.features.scale"],
                                       min=0.1,
                                       max=10,
                                       style=style)
        self.feature_columns.observe(self.regenerate, names='value')
        self.feature_scale.observe(self.regenerate, names='value')
        ## Hack to center SVG as justify-content is broken:
        self.net_svg = HTML(value="""<p style="text-align:center">%s</p>""" % ("",), layout=Layout(
            width=self._width, overflow_x='auto', overflow_y="auto",
            justify_content="center"))
        # Make controls first:
        self.output = Output()
        controls = self.make_controls()
        config = self.make_config()
        super().__init__([config, controls, self.net_svg, self.output])

    def propagate(self, inputs):
        """
        Propagate inputs through the dashboard view of the network.
        """
        if dynamic_pictures_check():
            return self.net.propagate(inputs, class_id=self.class_id, update_pictures=True)
        else:
            self.regenerate(inputs=input)

    def goto(self, position):
        if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0:
            return
        if self.control_select.value == "Train":
            length = len(self.net.dataset.train_inputs)
        elif self.control_select.value == "Test":
            length = len(self.net.dataset.test_inputs)
        #### Position it:
        if position == "begin":
            self.control_slider.value = 0
        elif position == "end":
            self.control_slider.value = length - 1
        elif position == "prev":
            if self.control_slider.value - 1 < 0:
                self.control_slider.value = length - 1 # wrap around
            else:
                self.control_slider.value = max(self.control_slider.value - 1, 0)
        elif position == "next":
            if self.control_slider.value + 1 > length - 1:
                self.control_slider.value = 0 # wrap around
            else:
                self.control_slider.value = min(self.control_slider.value + 1, length - 1)
        self.position_text.value = self.control_slider.value


    def change_select(self, change=None):
        """
        """
        self.update_control_slider(change)
        self.regenerate()

    def update_control_slider(self, change=None):
        self.net.config["dashboard.dataset"] = self.control_select.value
        if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0:
            self.total_text.value = "of 0"
            self.control_slider.value = 0
            self.position_text.value = 0
            self.control_slider.disabled = True
            self.position_text.disabled = True
            for child in self.control_buttons.children:
                if not hasattr(child, "icon") or child.icon != "refresh":
                    child.disabled = True
            return
        if self.control_select.value == "Test":
            self.total_text.value = "of %s" % len(self.net.dataset.test_inputs)
            minmax = (0, max(len(self.net.dataset.test_inputs) - 1, 0))
            if minmax[0] <= self.control_slider.value <= minmax[1]:
                pass # ok
            else:
                self.control_slider.value = 0
            self.control_slider.min = minmax[0]
            self.control_slider.max = minmax[1]
            if len(self.net.dataset.test_inputs) == 0:
                disabled = True
            else:
                disabled = False
        elif self.control_select.value == "Train":
            self.total_text.value = "of %s" % len(self.net.dataset.train_inputs)
            minmax = (0, max(len(self.net.dataset.train_inputs) - 1, 0))
            if minmax[0] <= self.control_slider.value <= minmax[1]:
                pass # ok
            else:
                self.control_slider.value = 0
            self.control_slider.min = minmax[0]
            self.control_slider.max = minmax[1]
            if len(self.net.dataset.train_inputs) == 0:
                disabled = True
            else:
                disabled = False
        self.control_slider.disabled = disabled
        self.position_text.disbaled = disabled
        self.position_text.value = self.control_slider.value
        for child in self.control_buttons.children:
            if not hasattr(child, "icon") or child.icon != "refresh":
                child.disabled = disabled

    def update_zoom_slider(self, change):
        if change["name"] == "value":
            self.net.config["svg_scale"] = self.zoom_slider.value
            self.regenerate()

    def update_position_text(self, change):
        # {'name': 'value', 'old': 2, 'new': 3, 'owner': IntText(value=3, layout=Layout(width='100%')), 'type': 'change'}
        self.control_slider.value = change["new"]

    def get_current_input(self):
        if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0:
            return self.net.dataset.train_inputs[self.control_slider.value]
        elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0:
            return self.net.dataset.test_inputs[self.control_slider.value]

    def get_current_targets(self):
        if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0:
            return self.net.dataset.train_targets[self.control_slider.value]
        elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0:
            return self.net.dataset.test_targets[self.control_slider.value]

    def update_slider_control(self, change):
        if len(self.net.dataset.inputs) == 0 or len(self.net.dataset.targets) == 0:
            self.total_text.value = "of 0"
            return
        if change["name"] == "value":
            self.position_text.value = self.control_slider.value
            if self.control_select.value == "Train" and len(self.net.dataset.train_targets) > 0:
                self.total_text.value = "of %s" % len(self.net.dataset.train_inputs)
                if self.net.model is None:
                    return
                if not dynamic_pictures_check():
                    self.regenerate(inputs=self.net.dataset.train_inputs[self.control_slider.value],
                                    targets=self.net.dataset.train_targets[self.control_slider.value])
                    return
                output = self.net.propagate(self.net.dataset.train_inputs[self.control_slider.value],
                                            class_id=self.class_id, update_pictures=True)
                if self.feature_bank.value in self.net.layer_dict.keys():
                    self.net.propagate_to_features(self.feature_bank.value, self.net.dataset.train_inputs[self.control_slider.value],
                                                   cols=self.feature_columns.value, scale=self.feature_scale.value, html=False)
                if self.net.config["show_targets"]:
                    if len(self.net.output_bank_order) == 1: ## FIXME: use minmax of output bank
                        self.net.display_component([self.net.dataset.train_targets[self.control_slider.value]],
                                                   "targets",
                                                   class_id=self.class_id,
                                                   minmax=(-1, 1))
                    else:
                        self.net.display_component(self.net.dataset.train_targets[self.control_slider.value],
                                                   "targets",
                                                   class_id=self.class_id,
                                                   minmax=(-1, 1))
                if self.net.config["show_errors"]: ## minmax is error
                    if len(self.net.output_bank_order) == 1:
                        errors = np.array(output) - np.array(self.net.dataset.train_targets[self.control_slider.value])
                        self.net.display_component([errors.tolist()],
                                                   "errors",
                                                   class_id=self.class_id,
                                                   minmax=(-1, 1))
                    else:
                        errors = []
                        for bank in range(len(self.net.output_bank_order)):
                            errors.append( np.array(output[bank]) - np.array(self.net.dataset.train_targets[self.control_slider.value][bank]))
                        self.net.display_component(errors, "errors",  class_id=self.class_id, minmax=(-1, 1))
            elif self.control_select.value == "Test" and len(self.net.dataset.test_targets) > 0:
                self.total_text.value = "of %s" % len(self.net.dataset.test_inputs)
                if self.net.model is None:
                    return
                if not dynamic_pictures_check():
                    self.regenerate(inputs=self.net.dataset.test_inputs[self.control_slider.value],
                                    targets=self.net.dataset.test_targets[self.control_slider.value])
                    return
                output = self.net.propagate(self.net.dataset.test_inputs[self.control_slider.value],
                                            class_id=self.class_id, update_pictures=True)
                if self.feature_bank.value in self.net.layer_dict.keys():
                    self.net.propagate_to_features(self.feature_bank.value, self.net.dataset.test_inputs[self.control_slider.value],
                                               cols=self.feature_columns.value, scale=self.feature_scale.value, html=False)
                if self.net.config["show_targets"]: ## FIXME: use minmax of output bank
                    self.net.display_component([self.net.dataset.test_targets[self.control_slider.value]],
                                               "targets",
                                               class_id=self.class_id,
                                               minmax=(-1, 1))
                if self.net.config["show_errors"]: ## minmax is error
                    if len(self.net.output_bank_order) == 1:
                        errors = np.array(output) - np.array(self.net.dataset.test_targets[self.control_slider.value])
                        self.net.display_component([errors.tolist()],
                                                   "errors",
                                                   class_id=self.class_id,
                                                   minmax=(-1, 1))
                    else:
                        errors = []
                        for bank in range(len(self.net.output_bank_order)):
                            errors.append( np.array(output[bank]) - np.array(self.net.dataset.test_targets[self.control_slider.value][bank]))
                        self.net.display_component(errors, "errors", class_id=self.class_id, minmax=(-1, 1))

    def toggle_play(self, button):
        ## toggle
        if self.button_play.description == "Play":
            self.button_play.description = "Stop"
            self.button_play.icon = "pause"
            self.player.resume()
        else:
            self.button_play.description = "Play"
            self.button_play.icon = "play"
            self.player.pause()

    def prop_one(self, button=None):
        self.update_slider_control({"name": "value"})

    def regenerate(self, button=None, inputs=None, targets=None):
        ## Protection when deleting object on shutdown:
        if isinstance(button, dict) and 'new' in button and button['new'] is None:
            return
        ## Update the config:
        self.net.config["dashboard.features.bank"] = self.feature_bank.value
        self.net.config["dashboard.features.columns"] = self.feature_columns.value
        self.net.config["dashboard.features.scale"] = self.feature_scale.value
        inputs = inputs if inputs is not None else self.get_current_input()
        targets = targets if targets is not None else self.get_current_targets()
        features = None
        if self.feature_bank.value in self.net.layer_dict.keys() and inputs is not None:
            if self.net.model is not None:
                features = self.net.propagate_to_features(self.feature_bank.value, inputs,
                                                          cols=self.feature_columns.value,
                                                          scale=self.feature_scale.value, display=False)
        svg = """<p style="text-align:center">%s</p>""" % (self.net.to_svg(
            inputs=inputs,
            targets=targets,
            class_id=self.class_id,
            highlights={self.feature_bank.value: {
                "border_color": "orange",
                "border_width": 30,
            }}))
        if inputs is not None and features is not None:
            html_horizontal = """
<table align="center" style="width: 100%%;">
 <tr>
  <td valign="top" style="width: 50%%;">%s</td>
  <td valign="top" align="center" style="width: 50%%;"><p style="text-align:center"><b>%s</b></p>%s</td>
</tr>
</table>"""
            html_vertical = """
<table align="center" style="width: 100%%;">
 <tr>
  <td valign="top">%s</td>
</tr>
<tr>
  <td valign="top" align="center"><p style="text-align:center"><b>%s</b></p>%s</td>
</tr>
</table>"""
            self.net_svg.value = (html_vertical if self.net.config["svg_rotate"] else html_horizontal) % (
                svg, "%s details" % self.feature_bank.value, features)
        else:
            self.net_svg.value = svg

    def make_colormap_image(self, colormap_name):
        from .layers import Layer
        if not colormap_name:
            colormap_name = get_colormap()
        layer = Layer("Colormap", 100)
        minmax = layer.get_act_minmax()
        image = layer.make_image(np.arange(minmax[0], minmax[1], .01),
                                 colormap_name,
                                 {"pixels_per_unit": 1,
                                  "svg_rotate": self.net.config["svg_rotate"]}).resize((300, 25))
        return image

    def set_attr(self, obj, attr, value):
        if value not in [{}, None]: ## value is None when shutting down
            if isinstance(value, dict):
                value = value["value"]
            if isinstance(obj, dict):
                obj[attr] = value
            else:
                setattr(obj, attr, value)
            ## was crashing on Widgets.__del__, if get_ipython() no longer existed
            self.regenerate()

    def make_controls(self):
        layout = Layout(width='100%', height="100%")
        button_begin = Button(icon="fast-backward", layout=layout)
        button_prev = Button(icon="backward", layout=layout)
        button_next = Button(icon="forward", layout=layout)
        button_end = Button(icon="fast-forward", layout=layout)
        #button_prop = Button(description="Propagate", layout=Layout(width='100%'))
        #button_train = Button(description="Train", layout=Layout(width='100%'))
        self.button_play = Button(icon="play", description="Play", layout=layout)
        step_down = Button(icon="sort-down", layout=Layout(width="95%", height="100%"))
        step_up = Button(icon="sort-up", layout=Layout(width="95%", height="100%"))
        up_down = HBox([step_down, step_up], layout=Layout(width="100%", height="100%"))
        refresh_button = Button(icon="refresh", layout=Layout(width="25%", height="100%"))

        self.position_text = IntText(value=0, layout=layout)

        self.control_buttons = HBox([
            button_begin,
            button_prev,
            #button_train,
            self.position_text,
            button_next,
            button_end,
            self.button_play,
            up_down,
            refresh_button
        ], layout=Layout(width='100%', height="100%"))
        length = (len(self.net.dataset.train_inputs) - 1) if len(self.net.dataset.train_inputs) > 0 else 0
        self.control_slider = IntSlider(description="Dataset index",
                                   continuous_update=False,
                                   min=0,
                                   max=max(length, 0),
                                   value=0,
                                   layout=Layout(width='100%'))
        if self.net.config["dashboard.dataset"] == "Train":
            length = len(self.net.dataset.train_inputs)
        else:
            length = len(self.net.dataset.test_inputs)
        self.total_text = Label(value="of %s" % length, layout=Layout(width="100px"))
        self.zoom_slider = FloatSlider(description="Zoom",
                                       continuous_update=False,
                                       min=0, max=1.0,
                                       style={"description_width": 'initial'},
                                       layout=Layout(width="65%"),
                                       value=self.net.config["svg_scale"] if self.net.config["svg_scale"] is not None else 0.5)

        ## Hook them up:
        button_begin.on_click(lambda button: self.goto("begin"))
        button_end.on_click(lambda button: self.goto("end"))
        button_next.on_click(lambda button: self.goto("next"))
        button_prev.on_click(lambda button: self.goto("prev"))
        self.button_play.on_click(self.toggle_play)
        self.control_slider.observe(self.update_slider_control, names='value')
        refresh_button.on_click(lambda widget: (self.update_control_slider(),
                                                self.output.clear_output(),
                                                self.regenerate()))
        step_down.on_click(lambda widget: self.move_step("down"))
        step_up.on_click(lambda widget: self.move_step("up"))
        self.zoom_slider.observe(self.update_zoom_slider, names='value')
        self.position_text.observe(self.update_position_text, names='value')
        # Put them together:
        controls = VBox([HBox([self.control_slider, self.total_text], layout=Layout(height="40px")),
                         self.control_buttons], layout=Layout(width='100%'))

        #net_page = VBox([control, self.net_svg], layout=Layout(width='95%'))
        controls.on_displayed(lambda widget: self.regenerate())
        return controls

    def move_step(self, direction):
        """
        Move the layer stepper up/down through network
        """
        options = [""] + [layer.name for layer in self.net.layers]
        index = options.index(self.feature_bank.value)
        if direction == "up":
            new_index = (index + 1) % len(options)
        else: ## down
            new_index = (index - 1) % len(options)
        self.feature_bank.value = options[new_index]
        self.regenerate()

    def make_config(self):
        layout = Layout()
        style = {"description_width": "initial"}
        checkbox1 = Checkbox(description="Show Targets", value=self.net.config["show_targets"],
                             layout=layout, style=style)
        checkbox1.observe(lambda change: self.set_attr(self.net.config, "show_targets", change["new"]), names='value')
        checkbox2 = Checkbox(description="Errors", value=self.net.config["show_errors"],
                             layout=layout, style=style)
        checkbox2.observe(lambda change: self.set_attr(self.net.config, "show_errors", change["new"]), names='value')

        hspace = IntText(value=self.net.config["hspace"], description="Horizontal space between banks:",
                         style=style, layout=layout)
        hspace.observe(lambda change: self.set_attr(self.net.config, "hspace", change["new"]), names='value')
        vspace = IntText(value=self.net.config["vspace"], description="Vertical space between layers:",
                         style=style, layout=layout)
        vspace.observe(lambda change: self.set_attr(self.net.config, "vspace", change["new"]), names='value')
        self.feature_bank = Select(description="Details:", value=self.net.config["dashboard.features.bank"],
                              options=[""] + [layer.name for layer in self.net.layers],
                              rows=1)
        self.feature_bank.observe(self.regenerate, names='value')
        self.control_select = Select(
            options=['Test', 'Train'],
            value=self.net.config["dashboard.dataset"],
            description='Dataset:',
            rows=1
        )
        self.control_select.observe(self.change_select, names='value')
        column1 = [self.control_select,
                   self.zoom_slider,
                   hspace,
                   vspace,
                   HBox([checkbox1, checkbox2]),
                   self.feature_bank,
                   self.feature_columns,
                   self.feature_scale
        ]
        ## Make layer selectable, and update-able:
        column2 = []
        layer = self.net.layers[-1]
        self.layer_select = Select(description="Layer:", value=layer.name,
                                   options=[layer.name for layer in
                                            self.net.layers],
                                   rows=1)
        self.layer_select.observe(self.update_layer_selection, names='value')
        column2.append(self.layer_select)
        self.layer_visible_checkbox = Checkbox(description="Visible", value=layer.visible, layout=layout)
        self.layer_visible_checkbox.observe(self.update_layer, names='value')
        column2.append(self.layer_visible_checkbox)
        self.layer_colormap = Select(description="Colormap:",
                                     options=[""] + AVAILABLE_COLORMAPS,
                                     value=layer.colormap if layer.colormap is not None else "", layout=layout, rows=1)
        self.layer_colormap_image = HTML(value="""<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap)))
        self.layer_colormap.observe(self.update_layer, names='value')
        column2.append(self.layer_colormap)
        column2.append(self.layer_colormap_image)
        ## get dynamic minmax; if you change it it will set it in layer as override:
        minmax = layer.get_act_minmax()
        self.layer_mindim = FloatText(description="Leftmost color maps to:", value=minmax[0], style=style)
        self.layer_maxdim = FloatText(description="Rightmost color maps to:", value=minmax[1], style=style)
        self.layer_mindim.observe(self.update_layer, names='value')
        self.layer_maxdim.observe(self.update_layer, names='value')
        column2.append(self.layer_mindim)
        column2.append(self.layer_maxdim)
        output_shape = layer.get_output_shape()
        self.layer_feature = IntText(value=layer.feature, description="Feature to show:", style=style)
        self.svg_rotate = Checkbox(description="Rotate", value=layer.visible, layout=layout)
        self.layer_feature.observe(self.update_layer, names='value')
        column2.append(self.layer_feature)
        self.svg_rotate = Checkbox(description="Rotate network",
                                   value=self.net.config["svg_rotate"],
                                   style={"description_width": 'initial'},
                                   layout=Layout(width="52%"))
        self.svg_rotate.observe(lambda change: self.set_attr(self.net.config, "svg_rotate", change["new"]), names='value')
        self.save_config_button = Button(icon="save", layout=Layout(width="10%"))
        self.save_config_button.on_click(self.save_config)
        column2.append(HBox([self.svg_rotate, self.save_config_button]))
        config_children = HBox([VBox(column1, layout=Layout(width="100%")),
                                VBox(column2, layout=Layout(width="100%"))])
        accordion = Accordion(children=[config_children])
        accordion.set_title(0, self.net.name)
        accordion.selected_index = None
        return accordion

    def save_config(self, widget=None):
        self.net.save_config()

    def update_layer(self, change):
        """
        Update the layer object, and redisplay.
        """
        if self._ignore_layer_updates:
            return
        ## The rest indicates a change to a display variable.
        ## We need to save the value in the layer, and regenerate
        ## the display.
        # Get the layer:
        layer = self.net[self.layer_select.value]
        # Save the changed value in the layer:
        layer.feature = self.layer_feature.value
        layer.visible = self.layer_visible_checkbox.value
        ## These three, dealing with colors of activations,
        ## can be done with a prop_one():
        if "color" in change["owner"].description.lower():
            ## Matches: Colormap, lefmost color, rightmost color
            ## overriding dynamic minmax!
            layer.minmax = (self.layer_mindim.value, self.layer_maxdim.value)
            layer.minmax = (self.layer_mindim.value, self.layer_maxdim.value)
            layer.colormap = self.layer_colormap.value if self.layer_colormap.value else None
            self.layer_colormap_image.value = """<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap))
            self.prop_one()
        else:
            self.regenerate()

    def update_layer_selection(self, change):
        """
        Just update the widgets; don't redraw anything.
        """
        ## No need to redisplay anything
        self._ignore_layer_updates = True
        ## First, get the new layer selected:
        layer = self.net[self.layer_select.value]
        ## Now, let's update all of the values without updating:
        self.layer_visible_checkbox.value = layer.visible
        self.layer_colormap.value = layer.colormap if layer.colormap != "" else ""
        self.layer_colormap_image.value = """<img src="%s"/>""" % self.net._image_to_uri(self.make_colormap_image(layer.colormap))
        minmax = layer.get_act_minmax()
        self.layer_mindim.value = minmax[0]
        self.layer_maxdim.value = minmax[1]
        self.layer_feature.value = layer.feature
        self._ignore_layer_updates = False
Ejemplo n.º 3
0
class DatabaseExplorer(VBox):
    """
    Combo widget based on a select box containing all experiments in
    specified database.
    """

    session = None
    ee = None
    experiments = None
    keywords = None
    variables = None

    def __init__(self, session=None, de=None):

        if session is None:
            session = database.create_session()
        self.session = session

        if de is not None:
            warning.warn(
                "DatabaseExtension has been deprecated is no longer supported")

        self.experiments = querying.get_experiments(session=self.session,
                                                    all=True)
        self.keywords = sorted(querying.get_keywords(self.session),
                               key=str.casefold)
        self.variables = querying.get_variables(self.session, inferred=True)

        self._make_widgets()

        # Call super init and pass widgets as children
        super().__init__(children=[
            self.header, self.selectors, self.expt_info, self.expt_explorer
        ])

        # Show the experiment information: important for only one experiment, as
        # events will not trigger this otherwise
        self._show_experiment_information(self.expt_selector.value)
        self._set_handlers()

    def _make_widgets(self):

        style = "<style>.header p{ line-height: 1.4; margin-bottom: 10px }</style>"

        # Gui header
        self.header = HTML(
            value=style + """
            <h3>Database Explorer</h3>

            <div class="header">

            <p>Select an experiment to show more detailed information where available.
            With an experiment selected push 'Load Experiment' to open an Experiment
            Explorer gui.</p>

            <p>The list of experiments can be filtered by keywords and/or variables.
            Multiple keywords can be selected using alt/option/ctrl (system dependent)
            or the shift modifier when selecting. To filter by variables select a
            variable and add it to the "Filter variables" box using the ">>" button,
            and vice-versa to remove variables from the filter. Push the 'Filter'
            button to show only matching experiments.</p>

            <p>When the ExperimentExplorer element loads data it is accessible as the
            <tt>.data</tt> attribute of the DatabaseExplorer object</p>

            </div>
            """,
            description="",
            layout={"width": "60%"},
        )

        # Experiment selector box
        self.expt_selector = Select(
            options=sorted(set(self.experiments.experiment), key=str.casefold),
            rows=24,
            layout={
                "padding": "0px 5px",
                "width": "auto"
            },
            disabled=False,
        )

        # Keyword filtering element is a Multiple selection box
        # checkboxes
        self.filter_widget = SelectMultiple(
            rows=15,
            options=sorted(self.keywords, key=str.casefold),
            layout={"flex": "0 0 100%"},
        )
        # Reset keywords button
        self.clear_keywords_button = Button(
            description="Clear",
            layout={
                "width": "20%",
                "align": "center"
            },
            tooltip="Click to clear selected keywords",
        )
        self.keyword_box = VBox(
            [self.filter_widget, self.clear_keywords_button],
            layout={"flex": "0 0 40%"})

        # Filtering button
        self.filter_button = Button(
            description="Filter",
            # layout={'width': '50%', 'align': 'center'},
            tooltip="Click to filter experiments",
        )

        # Variable filter selector combo widget
        self.var_filter = VariableSelectFilter(self.variables,
                                               layout={"flex": "0 0 40%"})

        # Tab box to contain keyword and variable filters
        self.filter_tabs = Tab(title="Filter",
                               children=[self.keyword_box, self.var_filter])
        self.filter_tabs.set_title(0, "Keyword")
        self.filter_tabs.set_title(1, "Variable")

        self.load_button = Button(
            description="Load Experiment",
            disabled=False,
            layout={
                "width": "50%",
            },
            tooltip="Click to load experiment",
        )

        # Experiment information panel
        self.expt_info = HTML(
            value="",
            description="",
            layout={
                "width": "80%",
                "align": "center"
            },
        )

        # Experiment explorer box
        self.expt_explorer = HBox()

        # Some box layout nonsense to organise widgets in space
        self.selectors = HBox([
            VBox(
                [
                    Label(value="Experiments:"),
                    self.expt_selector,
                    self.load_button,
                ],
                layout={
                    "padding": "0px 5px",
                    "flex": "0 0 30%"
                },
            ),
            VBox(
                [
                    Label(value="Filter by:"), self.filter_tabs,
                    self.filter_button
                ],
                layout={
                    "padding": "0px 10px",
                    "flex": "0 0 65%"
                },
            ),
        ])

    def _keyword_filter(self, keywords):
        """
        Return a list of experiments matching *all* of the supplied keywords
        """
        try:
            return querying.get_experiments(self.session,
                                            keywords=keywords).experiment
        except AttributeError:
            return []

    def _variable_filter(self, variables):
        """
        Return a set of experiments that contain all the defined variables
        """
        return querying.get_experiments(self.session,
                                        variables=variables).experiment

    def _set_handlers(self):
        """
        Define routines to handle button clicks and experiment selection
        """
        self.expt_selector.observe(self._expt_eventhandler, names="value")
        self.load_button.on_click(self._load_experiment)
        self.filter_button.on_click(self._filter_experiments)
        self.clear_keywords_button.on_click(self._clear_keywords)

    def _filter_restart_eventhandler(self, selector):
        """
        Re-populate variable list when checkboxes selected/de-selected
        """
        self._filter_variables()

    def _clear_keywords(self, selector):
        """
        Deselect all keywords
        """
        self.filter_widget.value = ()

    def _expt_eventhandler(self, selector):
        """
        When experiment is selected populate the experiment information
        elements
        """
        if selector.new is None:
            return
        self._show_experiment_information(selector.new)

    def _show_experiment_information(self, experiment_name):
        """
        Populate box with experiment information
        """
        expt = self.experiments[self.experiments.experiment == experiment_name]

        style = """
        <style>
            .info { font: normal 90% Verdana, Arial, sans-serif; }
            .info a:hover { color: red; text-decoration: underline; }
        </style>
        """
        self.expt_info.value = (style + """
        <div class="info">
        <table>
        <tr><td><b>Experiment:</b></td> <td>{experiment}</td></tr>
        <tr><td style="vertical-align:top;"><b>Description:</b></td> <td>{description}</td></tr>
        <tr><td style="vertical-align:top;"><b>Notes:</b></td> <td>{notes}</td></tr>
        <tr><td><b>Contact:</b></td> <td>{contact} &lt;<a href="mailto:{email}" target="_blank">{email}</a>&gt;</td></tr>
        <tr><td><b>Control repo:</b></td> <td><a href="{url}" target="_blank">{url}</a></td></tr>
        <tr><td><b>No. files:</b></td> <td>{ncfiles}</td></tr>
        <tr><td><b>Created:</b></td> <td>{created}</td></tr>
        </table>
        </div>
        """.format(experiment=experiment_name,
                   **{
                       field: return_value_or_empty(expt[field].values[0])
                       for field in [
                           "description",
                           "notes",
                           "contact",
                           "email",
                           "url",
                           "ncfiles",
                           "created",
                       ]
                   }))

    def _filter_experiments(self, b):
        """
        Filter experiment list by keywords and variable
        """
        options = set(self.experiments.experiment)

        kwds = self.filter_widget.value
        if len(kwds) > 0:
            options.intersection_update(self._keyword_filter(kwds))

        variables = self.var_filter.selected_vars()
        if len(variables) > 0:
            options.intersection_update(self._variable_filter(variables))

        self.expt_selector.options = sorted(options, key=str.casefold)

    def _load_experiment(self, b):
        """
        Open an Experiment Explorer UI with selected experiment
        """
        if self.expt_selector.value is not None:
            self.ee = ExperimentExplorer(session=self.session,
                                         experiment=self.expt_selector.value)
            self.expt_explorer.children = [self.ee]

    @property
    def data(self):
        """
        Return xarray DataArray if one has been loaded in ExperimentExplorer
        """
        if self.ee is None:
            print("Cannot return data if no experiment has been loaded")
            return None

        return self.ee.data
Ejemplo n.º 4
0
class VariableSelector(VBox):
    """
    Combo widget based on a Select box with a search panel above to live
    filter variables to Select. When a variable is selected the long name
    attribute is displayed under the select box. There are also two
    checkboxes which hide coordinates and restart variables.

    Note that a dict is used to populate the Select widget, so the visible
    value is the variable name and is accessed via the label attribute,
    and the long name via the value attribute.
    """

    variables = None

    def __init__(self, variables, rows=10, **kwargs):
        """
        variables is a pandas dataframe. kwargs are passed through to child
        widgets which, theoretically, allows for layout information to be
        specified
        """
        self._make_widgets(rows)
        super().__init__(children=[
            self.model,
            self.search,
            self.selector,
            self.info,
            self.filter_coords,
            self.filter_restarts,
        ],
                         **kwargs)
        self.set_variables(variables)
        self._set_info()
        self._set_observes()

    def _make_widgets(self, rows):
        """
        Instantiate all widgets
        """

        # Experiment selector element
        self.model = Dropdown(
            options=(),
            layout={
                "padding": "0px 5px",
                "width": "initial"
            },
            description="",
        )
        # Variable search
        self.search = Text(
            placeholder="Search: start typing",
            layout={
                "padding": "0px 5px",
                "width": "auto",
                "overflow-x": "scroll"
            },
        )
        # Variable selection box
        self.selector = Select(
            options=(),  # sorted(self.variables.name, key=str.casefold),
            rows=rows,
            layout=self.search.layout,
        )
        # Variable info
        self.info = HTML(layout=self.search.layout)
        # Variable filtering elements
        self.filter_coords = Checkbox(
            value=True,
            indent=False,
            description="Hide coordinates",
        )
        self.filter_restarts = Checkbox(
            value=True,
            indent=False,
            description="Hide restarts",
        )

    def _set_observes(self):
        """
        Set event handlers
        """
        self.filter_coords.observe(self._filter_eventhandler, names="value")
        self.filter_restarts.observe(self._filter_eventhandler, names="value")

        self.model.observe(self._model_eventhandler, names="value")
        self.search.observe(self._search_eventhandler, names="value")
        self.selector.observe(self._selector_eventhandler, names="value")

    def set_variables(self, variables):
        """
        Change variables
        """
        # Add a new column to keep track of visibility in widget
        self.variables = variables.assign(visible=True)

        # Set default filtering
        self._filter_variables()

        # Update selector
        self._update_selector(self.variables[self.variables.visible])

    def _update_selector(self, variables):
        """
        Update the variables in the selector. The variable are passed as an
        argument, so can differ from the internal variable list. This allows
        for easy filtering
        """
        # Populate model selector. Note label and value differ
        options = {"All models": ""}
        for model in variables.model.cat.categories.values:
            if len(model) > 0 and model != "none":
                options["{} only".format(model.capitalize())] = model
        self.model.options = options

        options = dict()
        firstvar = None
        for vals in variables.sort_values(["name"
                                           ])[["name", "long_name",
                                               "units"]].values:

            var, name, units = map(str, vals)

            if firstvar is None:
                firstvar = var

            if name.lower() == "none" or name == "":
                name = var

            # Add units string if suitable value exists
            if (units.lower() == "none" or units.lower() == "nounits"
                    or units.lower() == "no units"
                    or units.lower() == "dimensionless" or units.lower() == "1"
                    or units == ""):
                options[var] = "{}".format(name)
            else:
                options[var] = "{} ({})".format(name, units)

        # Populate variable selector
        self.selector.options = options

        # Highlight first value, otherwise accessors like .value are not
        # immediately accessible
        if firstvar is not None:
            self.selector.value = options[firstvar]

    def _reset_filters(self):
        """
        Reset filters to default values
        """
        self.filter_coords.value = True
        self.filter_restarts.value = True

    def _model_eventhandler(self, event=None):
        """
        Filter by model
        """
        model = self.model.value

        # Reset the coord and restart filters when a model changed
        self._reset_filters()
        self._filter_variables(model=model)

    def _filter_eventhandler(self, event=None):
        """
        Called when filter button pushed
        """
        self._filter_variables(self.filter_coords.value,
                               self.filter_restarts.value, self.model.value)

    def _filter_variables(self, coords=True, restarts=True, model=""):
        """
        Optionally hide some variables
        """
        # Set up a mask with all true values
        mask = self.variables["name"] != ""

        # Filter for matching models
        if model != "":
            mask = mask & (self.variables["model"] == model)

        # Conditionally filter out restarts and coordinates
        if coords:
            mask = mask & ~self.variables["coordinate"]
        if restarts:
            mask = mask & ~self.variables["restart"]

        # Mask out hidden variables
        self.variables["visible"] = mask

        # Update the variable selector
        self._update_selector(self.variables[self.variables.visible])

        # Reset the search
        self.search.value = ""
        self.selector.value = None

    def _search_eventhandler(self, event=None):
        """
        Live search bar, updates the selector options dynamically, does not alter
        visible mask in variables
        """
        search_term = self.search.value

        variables = self.variables[self.variables.visible]
        if search_term is not None or search_term != "":
            try:
                variables = variables[variables.name.str.contains(
                    search_term, case=False, na=False)
                                      | variables.long_name.str.contains(
                                          search_term, case=False, na=False)]
            except:
                warnings.warn("Illegal character in search!", UserWarning)
                search_term = self.search.value

        self._update_selector(variables)

    def _selector_eventhandler(self, event=None):
        """
        Update variable info when variable selected
        """
        self._set_info(self.selector.value)

    def _set_info(self, long_name=None):
        """
        Set long name info widget
        """
        if long_name is None or long_name == "":
            long_name = "&nbsp;"
        style = "<style>.breakword { word-wrap: break-word; font-size: 90%; line-height: 1.1;}</style>"
        self.info.value = style + '<p class="breakword">{long_name}</p>'.format(
            long_name=long_name)

    def delete(self, variable_names=None):
        """
        Remove variables
        """
        # If no variable specified just delete the currently selected one
        if variable_names is None:
            if self.selector.label is None:
                return None
            else:
                variable_names = [
                    self.selector.label,
                ]

        if isinstance(variable_names, str):
            variable_names = [
                variable_names,
            ]

        mask = self.variables["name"].isin(variable_names)
        deleted = self.variables[mask]

        # Delete variables
        self.variables = self.variables[~mask]

        # Update selector. Use search eventhandler so the selector preserves any
        # current search term. It is annoying to have that reset and type in again
        # if multiple variables are to be added
        self._search_eventhandler()

        return deleted

    def add(self, variables):
        """
        Add variables
        """
        # Concatenate existing and new variables
        self.variables = pd.concat([self.variables, variables])

        # Need to recalculate the visible flag as new variables have been added
        self._filter_eventhandler(None)

    def get_selected(self):
        """
        Return currently selected variable name
        """
        return self.selector.label
Ejemplo n.º 5
0
class FileChooser(VBox, ValueWidget):
    """FileChooser class."""

    _LBL_TEMPLATE = '<span style="margin-left:10px; color:{1};">{0}</span>'
    _LBL_NOFILE = 'No file selected'

    def __init__(self,
                 path=os.getcwd(),
                 filename='',
                 title='',
                 width=300,
                 select_desc='Select',
                 change_desc='Change',
                 show_hidden=False,
                 select_default=False,
                 use_dir_icons=False,
                 show_only_dirs=False,
                 filter_pattern=None,
                 **kwargs):
        """Initialize FileChooser object."""
        self._default_path = path.rstrip(os.path.sep)
        self._default_filename = filename
        self._selected_path = None
        self._selected_filename = None
        self._show_hidden = show_hidden
        self._select_desc = select_desc
        self._change_desc = change_desc
        self._callback = None
        self._select_default = select_default
        self._use_dir_icons = use_dir_icons
        self._show_only_dirs = show_only_dirs
        self._filter_pattern = filter_pattern

        # Widgets
        self._pathlist = Dropdown(description="",
                                  layout=Layout(width='auto',
                                                grid_area='pathlist'))
        self._filename = Text(
            placeholder='output filename',
            layout=Layout(width='auto',
                          grid_area='filename',
                          display=(None, "none")[self._show_only_dirs]),
            disabled=self._show_only_dirs)
        self._dircontent = Select(rows=8,
                                  layout=Layout(width='auto',
                                                grid_area='dircontent'))
        self._cancel = Button(description='Cancel',
                              layout=Layout(width='auto', display='none'))
        self._select = Button(description=self._select_desc,
                              layout=Layout(width='auto'))

        self._title = HTML(value=title)

        if title == '':
            self._title.layout.display = 'none'

        # Widget observe handlers
        self._pathlist.observe(self._on_pathlist_select, names='value')
        self._dircontent.observe(self._on_dircontent_select, names='value')
        self._filename.observe(self._on_filename_change, names='value')
        self._select.on_click(self._on_select_click)
        self._cancel.on_click(self._on_cancel_click)

        # Selected file label
        self._label = HTML(value=self._LBL_TEMPLATE.format(
            self._LBL_NOFILE, 'black'),
                           placeholder='',
                           description='')

        # Layout
        self._gb = VBox(
            children=[self._pathlist, self._filename, self._dircontent],
            layout=Layout(width=f'{width}px'))
        # self._gb = GridBox(
        #     children=[
        #         self._pathlist,
        #         self._filename,
        #         self._dircontent
        #     ],
        #     layout=Layout(
        #         display='none',
        #         width=f'{width}px',
        #         grid_gap='0px 0px',
        #         grid_template_rows='auto auto',
        #         grid_template_columns='60% 40%',
        #         grid_template_areas='''
        #             'pathlist {}'
        #             'dircontent dircontent'
        #             '''.format(('filename', 'pathlist')[self._show_only_dirs])
        #     )
        # )

        # buttonbar = HBox(
        #     children=[
        #         self._select,
        #         self._cancel,
        #         self._label
        #     ],
        #     layout=Layout(width='auto')
        # )
        buttonbar = VBox(children=[self._select, self._cancel, self._label],
                         layout=Layout(width='auto'))

        # Call setter to set initial form values
        self._set_form_values(self._default_path, self._default_filename)

        # Use the defaults as the selected values
        if self._select_default:
            self._apply_selection()

        # Call VBox super class __init__
        super().__init__(children=[
            self._title,
            self._gb,
            buttonbar,
        ],
                         layout=Layout(width='auto'),
                         **kwargs)

    def _set_form_values(self, path, filename):
        """Set the form values."""
        # Disable triggers to prevent selecting an entry in the Select
        # box from automatically triggering a new event.
        self._pathlist.unobserve(self._on_pathlist_select, names='value')
        self._dircontent.unobserve(self._on_dircontent_select, names='value')
        self._filename.unobserve(self._on_filename_change, names='value')

        # In folder only mode zero out the filename
        if self._show_only_dirs:
            filename = ''

        # Set form values
        self._pathlist.options = get_subpaths(path)
        self._pathlist.value = path
        self._filename.value = filename

        # file/folder real names
        dircontent_real_names = get_dir_contents(
            path,
            show_hidden=self._show_hidden,
            prepend_icons=False,
            show_only_dirs=self._show_only_dirs,
            filter_pattern=self._filter_pattern)

        # file/folder display names
        dircontent_display_names = get_dir_contents(
            path,
            show_hidden=self._show_hidden,
            prepend_icons=self._use_dir_icons,
            show_only_dirs=self._show_only_dirs,
            filter_pattern=self._filter_pattern)

        # Dict to map real names to display names
        self._map_name_to_disp = {
            real_name: disp_name
            for real_name, disp_name in zip(dircontent_real_names,
                                            dircontent_display_names)
        }

        # Dict to map display names to real names
        self._map_disp_to_name = dict(
            reversed(item) for item in self._map_name_to_disp.items())

        # Set _dircontent form value to display names
        self._dircontent.options = dircontent_display_names

        # If the value in the filename Text box equals a value in the
        # Select box and the entry is a file then select the entry.
        if ((filename in dircontent_real_names)
                and os.path.isfile(os.path.join(path, filename))):
            self._dircontent.value = self._map_name_to_disp[filename]
        else:
            self._dircontent.value = None

        # Reenable triggers again
        self._pathlist.observe(self._on_pathlist_select, names='value')
        self._dircontent.observe(self._on_dircontent_select, names='value')
        self._filename.observe(self._on_filename_change, names='value')

        # Update the state of the select button
        if self._gb.layout.display is None:
            # Disable the select button if path and filename
            # - equal an existing folder in the current view
            # - equal the already selected values
            # - don't match the provided filter pattern(s)
            check1 = filename in dircontent_real_names
            check2 = os.path.isdir(os.path.join(path, filename))
            check3 = False
            check4 = False

            # Only check selected if selected is set
            if ((self._selected_path is not None)
                    and (self._selected_filename is not None)):
                selected = os.path.join(self._selected_path,
                                        self._selected_filename)
                check3 = os.path.join(path, filename) == selected

            # Ensure only allowed extensions are used
            if self._filter_pattern:
                check4 = not match_item(filename, self._filter_pattern)

            if (check1 and check2) or check3 or check4:
                self._select.disabled = True
            else:
                self._select.disabled = False

    def _on_pathlist_select(self, change):
        """Handle selecting a path entry."""
        self._set_form_values(change['new'], self._filename.value)

    def _on_dircontent_select(self, change):
        """Handle selecting a folder entry."""
        new_path = os.path.realpath(
            os.path.join(self._pathlist.value,
                         self._map_disp_to_name[change['new']]))

        # Check if folder or file
        if os.path.isdir(new_path):
            path = new_path
            filename = self._filename.value
        elif os.path.isfile(new_path):
            path = self._pathlist.value
            filename = self._map_disp_to_name[change['new']]

        self._set_form_values(path, filename)

    def _on_filename_change(self, change):
        """Handle filename field changes."""
        self._set_form_values(self._pathlist.value, change['new'])

    def _on_select_click(self, _b):
        """Handle select button clicks."""
        if self._gb.layout.display == 'none':
            # If not shown, open the dialog
            self._show_dialog()
        else:
            # If shown, close the dialog and apply the selection
            self._apply_selection()

            # Execute callback function
            if self._callback is not None:
                try:
                    self._callback(self)
                except TypeError:
                    # Support previous behaviour of not passing self
                    self._callback()

    def _show_dialog(self):
        """Show the dialog."""
        # Show dialog and cancel button
        self._gb.layout.display = None
        self._cancel.layout.display = None

        # Show the form with the correct path and filename
        if ((self._selected_path is not None)
                and (self._selected_filename is not None)):
            path = self._selected_path
            filename = self._selected_filename
        else:
            path = self._default_path
            filename = self._default_filename

        self._set_form_values(path, filename)

    def _apply_selection(self):
        """Close the dialog and apply the selection."""
        self._gb.layout.display = 'none'
        self._cancel.layout.display = 'none'
        self._select.description = self._change_desc
        self._selected_path = self._pathlist.value
        self._selected_filename = self._filename.value

        selected = os.path.join(self._selected_path, self._selected_filename)

        if os.path.isfile(selected):
            self._label.value = self._LBL_TEMPLATE.format(selected, 'orange')
        else:
            self._label.value = self._LBL_TEMPLATE.format(selected, 'green')

    def _on_cancel_click(self, _b):
        """Handle cancel button clicks."""
        self._gb.layout.display = 'none'
        self._cancel.layout.display = 'none'
        self._select.disabled = False

    def reset(self, path=None, filename=None):
        """Reset the form to the default path and filename."""
        self._selected_path = None
        self._selected_filename = None

        # Reset select button and label
        self._select.description = self._select_desc
        self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE,
                                                      'black')

        if path is not None:
            self._default_path = path.rstrip(os.path.sep)

        if filename is not None:
            self._default_filename = filename

        # Set a proper filename value
        if self._show_only_dirs:
            filename = ''
        else:
            filename = self._default_filename

        self._set_form_values(self._default_path, filename)

        # Use the defaults as the selected values
        if self._select_default:
            self._apply_selection()

    def refresh(self):
        """Re-render the form."""
        self._set_form_values(self._pathlist.value, self._filename.value)

    @property
    def show_hidden(self):
        """Get _show_hidden value."""
        return self._show_hidden

    @show_hidden.setter
    def show_hidden(self, hidden):
        """Set _show_hidden value."""
        self._show_hidden = hidden
        self.refresh()

    @property
    def use_dir_icons(self):
        """Get _use_dir_icons value."""
        return self._use_dir_icons

    @use_dir_icons.setter
    def use_dir_icons(self, dir_icons):
        """Set _use_dir_icons value."""
        self._use_dir_icons = dir_icons
        self.refresh()

    @property
    def rows(self):
        """Get current number of rows."""
        return self._dircontent.rows

    @rows.setter
    def rows(self, rows):
        """Set number of rows."""
        self._dircontent.rows = rows

    @property
    def title(self):
        """Get the title."""
        return self._title.value

    @title.setter
    def title(self, title):
        """Set the title."""
        self._title.value = title

        if title == '':
            self._title.layout.display = 'none'
        else:
            self._title.layout.display = None

    @property
    def default(self):
        """Get the default value."""
        return os.path.join(self._default_path, self._default_filename)

    @property
    def default_path(self):
        """Get the default_path value."""
        return self._default_path

    @default_path.setter
    def default_path(self, path):
        """Set the default_path."""
        self._default_path = path.rstrip(os.path.sep)
        self._set_form_values(self._default_path, self._filename.value)

    @property
    def default_filename(self):
        """Get the default_filename value."""
        return self._default_filename

    @default_filename.setter
    def default_filename(self, filename):
        """Set the default_filename."""
        self._default_filename = filename
        self._set_form_values(self._pathlist.value, self._default_filename)

    @property
    def show_only_dirs(self):
        """Get show_only_dirs property value."""
        return self._show_only_dirs

    @show_only_dirs.setter
    def show_only_dirs(self, show_only_dirs):
        """Set show_only_dirs property value."""
        self._show_only_dirs = show_only_dirs

        # Update widget layout
        self._filename.disabled = self._show_only_dirs
        self._filename.layout.display = (None, "none")[self._show_only_dirs]
        self._gb.layout.children = [self._pathlist, self._dircontent]

        if not self._show_only_dirs:
            self._gb.layout.children.insert(1, self._filename)

        self._gb.layout.grid_template_areas = '''
            'pathlist {}'
            'dircontent dircontent'
            '''.format(('filename', 'pathlist')[self._show_only_dirs])

        # Reset the dialog
        self.reset()

    @property
    def filter_pattern(self):
        """Get file name filter pattern."""
        return self._filter_pattern

    @filter_pattern.setter
    def filter_pattern(self, filter_pattern):
        """Set file name filter pattern."""
        self._filter_pattern = filter_pattern
        self.refresh()

    @property
    def selected(self):
        """Get selected value."""
        try:
            return os.path.join(self._selected_path, self._selected_filename)
        except TypeError:
            return None

    @property
    def selected_path(self):
        """Get selected_path value."""
        return self._selected_path

    @property
    def selected_filename(self):
        """Get the selected_filename."""
        return self._selected_filename

    def __repr__(self):
        """Build string representation."""
        str_ = ("FileChooser("
                "path='{0}', "
                "filename='{1}', "
                "title='{2}', "
                "show_hidden='{3}', "
                "use_dir_icons='{4}', "
                "show_only_dirs='{5}', "
                "select_desc='{6}', "
                "change_desc='{7}')").format(
                    self._default_path, self._default_filename, self._title,
                    self._show_hidden, self._use_dir_icons,
                    self._show_only_dirs, self._select_desc, self._change_desc)
        return str_

    def register_callback(self, callback):
        """Register a callback function."""
        self._callback = callback

    def get_interact_value(self):
        """Return the value which should be passed to interactive functions."""
        return self.selected
Ejemplo n.º 6
0
class FileChooser(VBox):

    _LBL_TEMPLATE = '<span style="margin-left:10px; color:{1};">{0}</span>'
    _LBL_NOFILE = 'No file selected'

    def __init__(self,
                 path=os.getcwd(),
                 filename='',
                 show_hidden=False,
                 **kwargs):

        self._default_path = path.rstrip(os.path.sep)
        self._default_filename = filename
        self._selected_path = ''
        self._selected_filename = ''
        self._show_hidden = show_hidden

        # Widgets
        self._pathlist = Dropdown(description="",
                                  layout=Layout(width='auto',
                                                grid_area='pathlist'))
        self._filename = Text(placeholder='output filename',
                              layout=Layout(width='auto',
                                            grid_area='filename'))
        self._dircontent = Select(rows=8,
                                  layout=Layout(width='auto',
                                                grid_area='dircontent'))
        self._cancel = Button(description='Cancel',
                              layout=Layout(width='auto', display='none'))
        self._select = Button(description='Select',
                              layout=Layout(width='auto'))

        # Widget observe handlers
        self._pathlist.observe(self._on_pathlist_select, names='value')
        self._dircontent.observe(self._on_dircontent_select, names='value')
        self._filename.observe(self._on_filename_change, names='value')
        self._select.on_click(self._on_select_click)
        self._cancel.on_click(self._on_cancel_click)

        # Selected file label
        self._label = HTML(value=self._LBL_TEMPLATE.format(
            self._LBL_NOFILE, 'black'),
                           placeholder='',
                           description='')

        # Layout
        self._gb = GridBox(
            children=[self._pathlist, self._filename, self._dircontent],
            layout=Layout(display='none',
                          width='500px',
                          grid_gap='0px 0px',
                          grid_template_rows='auto auto',
                          grid_template_columns='60% 40%',
                          grid_template_areas='''
                    'pathlist filename'
                    'dircontent dircontent'
                    '''))
        buttonbar = HBox(children=[self._select, self._cancel, self._label],
                         layout=Layout(width='auto'))

        # Call setter to set initial form values
        self._set_form_values(self._default_path, self._default_filename)

        # Call VBox super class __init__
        super().__init__(children=[
            self._gb,
            buttonbar,
        ],
                         layout=Layout(width='auto'),
                         **kwargs)

    def _set_form_values(self, path, filename):
        '''Set the form values'''

        # Disable triggers to prevent selecting an entry in the Select
        # box from automatically triggering a new event.
        self._pathlist.unobserve(self._on_pathlist_select, names='value')
        self._dircontent.unobserve(self._on_dircontent_select, names='value')
        self._filename.unobserve(self._on_filename_change, names='value')

        # Set form values
        self._pathlist.options = get_subpaths(path)
        self._pathlist.value = path
        self._filename.value = filename
        self._dircontent.options = get_dir_contents(path,
                                                    hidden=self._show_hidden)

        # If the value in the filename Text box equals a value in the
        # Select box and the entry is a file then select the entry.
        if ((filename in self._dircontent.options)
                and os.path.isfile(os.path.join(path, filename))):
            self._dircontent.value = filename
        else:
            self._dircontent.value = None

        # Reenable triggers again
        self._pathlist.observe(self._on_pathlist_select, names='value')
        self._dircontent.observe(self._on_dircontent_select, names='value')
        self._filename.observe(self._on_filename_change, names='value')

        # Set the state of the select Button
        if self._gb.layout.display is None:
            selected = os.path.join(self._selected_path,
                                    self._selected_filename)

            # filename value is empty or equals the selected value
            if (filename == '') or (os.path.join(path, filename) == selected):
                self._select.disabled = True
            else:
                self._select.disabled = False

    def _on_pathlist_select(self, change):
        '''Handler for when a new path is selected'''
        self._set_form_values(change['new'], self._filename.value)

    def _on_dircontent_select(self, change):
        '''Handler for when a folder entry is selected'''
        new_path = update_path(self._pathlist.value, change['new'])

        # Check if folder or file
        if os.path.isdir(new_path):
            path = new_path
            filename = self._filename.value
        elif os.path.isfile(new_path):
            path = self._pathlist.value
            filename = change['new']

        self._set_form_values(path, filename)

    def _on_filename_change(self, change):
        '''Handler for when the filename field changes'''
        self._set_form_values(self._pathlist.value, change['new'])

    def _on_select_click(self, b):
        '''Handler for when the select button is clicked'''
        if self._gb.layout.display is 'none':
            self._gb.layout.display = None
            self._cancel.layout.display = None

            # Show the form with the correct path and filename
            if self._selected_path and self._selected_filename:
                path = self._selected_path
                filename = self._selected_filename
            else:
                path = self._default_path
                filename = self._default_filename

            self._set_form_values(path, filename)

        else:
            self._gb.layout.display = 'none'
            self._cancel.layout.display = 'none'
            self._select.description = 'Change'
            self._selected_path = self._pathlist.value
            self._selected_filename = self._filename.value
            # self._default_path = self._selected_path
            # self._default_filename = self._selected_filename

            selected = os.path.join(self._selected_path,
                                    self._selected_filename)

            if os.path.isfile(selected):
                self._label.value = self._LBL_TEMPLATE.format(
                    selected, 'orange')
            else:
                self._label.value = self._LBL_TEMPLATE.format(
                    selected, 'green')

    def _on_cancel_click(self, b):
        '''Handler for when the cancel button is clicked'''
        self._gb.layout.display = 'none'
        self._cancel.layout.display = 'none'
        self._select.disabled = False

    def reset(self, path=None, filename=None):
        '''Reset the form to the default path and filename'''
        self._selected_path = ''
        self._selected_filename = ''

        self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE,
                                                      'black')

        if path is not None:
            self._default_path = path.rstrip(os.path.sep)

        if filename is not None:
            self._default_filename = filename

        self._set_form_values(self._default_path, self._default_filename)

    def refresh(self):
        '''Re-render the form'''
        self._set_form_values(self._pathlist.value, self._filename.value)

    @property
    def show_hidden(self):
        '''Get current number of rows'''
        return self._show_hidden

    @show_hidden.setter
    def show_hidden(self, hidden):
        '''Set number of rows'''
        self._show_hidden = hidden
        self.refresh()

    @property
    def rows(self):
        '''Get current number of rows'''
        return self._dircontent.rows

    @rows.setter
    def rows(self, rows):
        '''Set number of rows'''
        self._dircontent.rows = rows

    @property
    def default(self):
        '''Get the default value'''
        return os.path.join(self._default_path, self._default_filename)

    @property
    def default_path(self):
        '''Get the default_path value'''
        return self._default_path

    @default_path.setter
    def default_path(self, path):
        '''Set the default_path'''
        self._default_path = path.rstrip(os.path.sep)
        self._default = os.path.join(self._default_path, self._filename.value)
        self._set_form_values(self._default_path, self._filename.value)

    @property
    def default_filename(self):
        '''Get the default_filename value'''
        return self._default_filename

    @default_filename.setter
    def default_filename(self, filename):
        '''Set the default_filename'''
        self._default_filename = filename
        self._default = os.path.join(self._pathlist.value,
                                     self._default_filename)
        self._set_form_values(self._pathlist.value, self._default_filename)

    @property
    def selected(self):
        '''Get selected value'''
        return os.path.join(self._selected_path, self._selected_filename)

    @property
    def selected_path(self):
        '''Get selected_path value'''
        return self._selected_path

    @property
    def selected_filename(self):
        '''Get the selected_filename'''
        return self._selected_filename

    def __repr__(self):
        str_ = ("FileChooser("
                "path='{0}', "
                "filename='{1}', "
                "show_hidden='{2}')").format(self._default_path,
                                             self._default_filename,
                                             self._show_hidden)
        return str_