from __future__ import (division, print_function, absolute_import,
unicode_literals)
import ipywidgets as widgets
from traitlets import link
__all__ = [
'ToggleContainer',
'ToggleMinMax',
'ToggleGo',
'set_color_for',
]
[docs]class ToggleContainer(widgets.FlexBox):
"""
A widget whose state controls the visibility of its chilren.
Parameters
----------
Same as parameters for a `~IPython.html.widgets.Box`, but
note that the description of the ToggleContainer is used to set the
description of the checkbox that controls the display, AND
toggle_type : {'checkbox', 'button'}, optional
Specify the type of boolean widget used to toggle the display
Attributes
----------
container : ContainerWidget
Object to which children should be added.
toggle : ToggleButtonWidget or CheckboxWidget
The toggle object, provided primarily to allow styling of it.
disabled : bool
Gets and sets whether the entire widget is disabled, i.e. the toggle
box and all children of this widget controlled by the toggle.
Notes
-----
Do *NOT* set the children of the ToggleContainer; set the children
of ``ToggleContainer.children`` or use the `add_child` method.
"""
def __init__(self, *args, **kwd):
toggle_types = {'checkbox': widgets.Checkbox,
'button': widgets.ToggleButton}
toggle_type = kwd.pop('toggle_type', 'checkbox')
if toggle_type not in toggle_types:
raise ValueError('toggle_type must be one of '
'{}'.format(toggle_type.keys()))
super(ToggleContainer, self).__init__(*args, **kwd)
self._toggle_container = widgets.Box(description='toggle holder')
self._checkbox = toggle_types[toggle_type](description=self.description)
self._toggle_container.children = [self._checkbox]
self._container = widgets.Box(description="Toggle-able container")
self._state_monitor = widgets.Checkbox(visible=False)
self.children = [
self._toggle_container,
self._container,
self._state_monitor
]
self._link_children()
self._child_notify_parent_on_change(self.toggle)
def __str__(self):
# Build up a list of strings of top level and children
yes_no = lambda x: 'Yes' if x else 'No'
base = ' '.join([self.description, yes_no(self.toggle.value)])
if self.toggle.value:
child_strings = ['\t' + str(child) for child in self.container.children]
indented_kids = [s.replace('\n\t', '\n\t\t') for s in child_strings]
base += '\n' + '\n'.join(indented_kids)
return base
@property
def container(self):
"""
Widget that contains the elements controlled by the toggle.
"""
return self._container
@property
def toggle(self):
"""
Toggle widget that controls other display elements.
"""
return self._checkbox
def _link_children(self):
"""
Links the visible property of the container to the toggle value.
"""
link((self._checkbox, str('value')), (self._container, str('visible')))
@property
def disabled(self):
"""
True if widget is disabled.
"""
return self._state_monitor.disabled
@disabled.setter
def disabled(self, value):
# Not every widget has a disabled attribute (e.g. ContainerWidget),
# but we can go down through children recursively if needed.
def set_disabled(child, value):
if hasattr(child, 'disabled'):
child.disabled = value
elif hasattr(child, 'children'):
for grandkid in child.children:
set_disabled(grandkid, value)
# someday test for boolean in here....
self._state_monitor.disabled = value
self.toggle.disabled = value
for child in self.container.children:
set_disabled(child, value)
@property
def is_sane(self):
"""
Subclasses can define a method that indicates whether the
current combination of settings is sensible.
Returns
-------
sanity : bool or None
True if the settings are sensible, False if not, None if not
overridden.
"""
return None
[docs] def display(self):
"""
Display and format this widget.
"""
from IPython.display import display
display(self)
self.format()
[docs] def add_child(self, child):
"""
Append a child to the container part of the widget.
Parameters
----------
child : IPython widget
"""
temp = list(self.container.children)
temp.append(child)
self.container.children = temp
self._child_notify_parent_on_change(child)
[docs] def action(self):
"""
Subclasses should override this method if they wish to associate an
action with the widget.
"""
pass
def _child_notify_parent_on_change(self, child):
# For some reason DropdownWidgets do not have a value key, only a
# value_name key. Seems inconsistent with the rest of the
# widgets, but whatever...
if isinstance(child, ToggleContainer):
# If the child is a ToggleContainer we only need to link in
# to its _state_monitor because it will take care hooking up the
# things in the container to its _state_monitor.
child._state_monitor.on_trait_change(self._ping_handler(),
str('value'))
elif str('value') in child.traits():
# If the child has a value trait we want to monitor it.
child.on_trait_change(self._ping_handler(), str('value'))
else:
# Might be a plain ContainerWidget (or tab or accordion), so look
# for its children.
try:
for grandchild in child.children:
self._child_notify_parent_on_change(grandchild)
except AttributeError:
pass
def _ping_handler(self):
def flip_state():
self._state_monitor.value = not self._state_monitor.value
return flip_state
[docs]class ToggleMinMax(ToggleContainer):
"""
Widget for setting a minimum and maximum integer value, controlled by
a toggle.
Parameters
----------
description : str
Text to be displayed in the toggle.
"""
def __init__(self, *args, **kwd):
super(ToggleMinMax, self).__init__(*args, **kwd)
self._min_box = widgets.FloatText(description="Low threshold")
self._max_box = widgets.FloatText(description="High threshold")
self.add_child(self._min_box)
self.add_child(self._max_box)
@property
def min(self):
"""
Minimum value in the widget.
"""
return self._min_box.value
@property
def max(self):
"""
Maximum value in the widget.
"""
return self._max_box.value
def __str__(self):
# Build up a list of strings of top level and children
yes_no = lambda x: 'Yes' if x else 'No'
base = ' '.join([self.description, yes_no(self.toggle.value)])
if self.toggle.value:
child_strings = ['\t' + str(child.description) + ': ' + str(child.value) for child in self.container.children]
indented_kids = [s.replace('\n\t', '\n\t\t') for s in child_strings]
base += '\n' + '\n'.join(indented_kids)
return base
[docs]class ToggleGo(ToggleContainer):
"""
ToggleContainer whose state is linked to a button.
The intent is for that button to be activated when the contents
of the container are in a "sane" state.
"""
def __init__(self, *args, **kwd):
super(ToggleGo, self).__init__(*args, **kwd)
self._go_container = widgets.HBox(visible=self.toggle.value)
self._go_button = widgets.Button(description="Lock settings and Go!",
disabled=True, visible=False)
self._change_settings = widgets.Button(description="Unlock settings",
disabled=True,
visible=False)
self._go_container.children = [self._go_button, self._change_settings]
self._progress_container = widgets.Box()
self._progress_bar = widgets.FloatProgress(min=0, max=1.0,
step=0.01, value=0.0,
visible=False)
self._progress_container.children = [self._progress_bar]
# we want the go button to be in a container below the
# ToggleContainer's container -- actually, no, want these
# buttons controlled by toggle...wait, no, I really do want that, but
# I also want to tie their visibility to the toggle.
kids = list(self.children)
kids.append(self._go_container)
kids.append(self._progress_container)
self.children = kids
# Tie visibility of go button to toggle state. Needs to be separate
# from the container.
link((self._go_container, str('visible')), (self.toggle, str('value')))
self._go_button.on_click(self.go())
self._change_settings.on_click(self.unlock())
# Tie self._state_monitor to both go button and color of toggle button
self._state_monitor.on_trait_change(self.state_change_handler(),
str('value'))
self._state_monitor.on_trait_change(set_color_for(self), str('value'))
@property
def is_sane(self):
# There are two ways to be insane here:
# 1. No steps are selected
for child in self.container.children:
try:
if child.toggle.value:
break
except AttributeError:
# The child isn't a toggle, apparently...
pass
else:
# Reminder: this executes if the loop completes successfully,
# i.e. none of the children are selected
return False
# 2. There is a combination of settings that doesn't make sense.
mental_state = [child.is_sane for child
in self.container.children
if hasattr(child, 'is_sane')
and child.is_sane is not None]
sanity = all(mental_state)
return sanity
@property
def progress_bar(self):
return self._progress_bar
[docs] def state_change_handler(self):
"""
Ties sanity state to go button controls and others
"""
def change_handler():
if self.is_sane is None:
return
# Sorry about the double negative below, but the IPython widget
# method is named DISabled...
self._go_button.disabled = not self.is_sane
self._go_button.visible = self.is_sane
return change_handler
[docs] def go(self):
"""
Returns the action to be taken when the "Go" button is clicked.
"""
def handler(b):
"""
b is the button pressed
"""
self.disabled = True
self._go_button.disabled = True
print(self)
self.action()
# change button should really only appear after the work is done.
self._go_button.width = '68%'
self._change_settings.visible = True
self._change_settings.disabled = False
return handler
[docs] def unlock(self):
"""
Handler for the unlock button.
"""
def handler(b):
self.disabled = False
self._go_button.disabled = False
self._change_settings.visible = False
self._go_button.width = '100%'
return handler
[docs] def action(self):
"""
The default action is to invoke the action of each child with an
update of the progress bar along the way.
"""
self.progress_bar.visible = True
self.progress_bar.value = 0
for idx, child in enumerate(self.container.children):
self.progress_bar.value = (idx + 1) / (len(self.children) + 1)
try:
child.action()
except AttributeError:
pass
self.progress_bar.value = (idx + 1)/len(self.children)
# Sleep for a bit so we can see progress happening.
self.progress_bar.visible = False
[docs]def set_color_for(a_widget):
def set_color(name, value):
if a_widget.toggle.value:
if not a_widget.is_sane:
a_widget.toggle.button_style = 'warning'
else:
a_widget.toggle.button_style = 'success'
else:
a_widget.toggle.button_style = None
return set_color