from abc import abstractmethod
from ipywidgets import widgets, Layout, ValueWidget, link, HBox
from ipywidgets.widgets.widget_description import DescriptionWidget
[docs]class WindowController(HBox, ValueWidget, DescriptionWidget):
def __init__(
self,
vmin,
vmax,
start_value=(None, None),
description="window (s)",
orientation="horizontal",
**kwargs
):
if orientation not in ("horizontal", "vertical"):
ValueError("Unrecognized orientation: {}".format(orientation))
super().__init__()
self.vmin = vmin
self.vmax = vmax
self.start_value = start_value
self.description = description
self.orientation = orientation
if self.orientation == "horizontal":
self.to_start_button = widgets.Button(
description="◀◀", layout=Layout(width="65px")
)
self.backwards_button = widgets.Button(
description="◀", layout=Layout(width="40px")
)
self.forward_button = widgets.Button(
description="▶", layout=Layout(width="40px")
)
self.to_end_button = widgets.Button(
description="▶▶", layout=Layout(width="65px")
)
else: # vertical
self.to_end_button = widgets.Button(
description="▲▲", layout=Layout(width="50px")
)
self.forward_button = widgets.Button(
description="▲", layout=Layout(width="50px")
)
self.backwards_button = widgets.Button(
description="▼", layout=Layout(width="50px")
)
self.to_start_button = widgets.Button(
description="▼▼", layout=Layout(width="50px")
)
self.to_start_button.on_click(self.move_start)
self.backwards_button.on_click(self.move_down)
self.forward_button.on_click(self.move_up)
self.to_end_button.on_click(self.move_end)
# @abstractmethod
# @property
# def value(self):
# """Must be window in seconds"""
# pass
[docs] @abstractmethod
def get_children(self):
pass
[docs] @abstractmethod
def move_up(self, change):
pass
[docs] @abstractmethod
def move_down(self, change):
pass
[docs] @abstractmethod
def move_start(self, change):
pass
[docs] @abstractmethod
def move_end(self, change):
pass
[docs]class RangeController(WindowController):
def __init__(
self,
vmin,
vmax,
start_value=None,
dtype="float",
description="time window (s)",
orientation="horizontal",
**kwargs
):
super().__init__(vmin, vmax, start_value, description, orientation, **kwargs)
self.dtype = dtype
self.slider = self.make_range_slider(description=description, **kwargs)
link((self.slider, "value"), (self, "value"))
link((self.slider, "description"), (self, "description"))
self.children = self.get_children()
[docs] def make_range_slider(self, **kwargs):
"""
Parameters
----------
kwargs: passed into RangeSlider constructor
Returns
-------
"""
slider_kwargs = dict(
value=self.start_value,
min=self.vmin,
max=self.vmax,
continuous_update=False,
readout=True,
style={"description_width": "initial"},
orientation=self.orientation,
)
if self.dtype == "float":
slider_kwargs.update(
readout_format=".1f",
step=0.1,
description="time window (s)",
layout=Layout(width="100%"),
)
slider_kwargs.update(kwargs)
return widgets.FloatRangeSlider(**slider_kwargs)
elif self.dtype == "int":
slider_kwargs.update(
description="unit window", layout=Layout(height="100%")
)
slider_kwargs.update(kwargs)
return widgets.IntRangeSlider(**slider_kwargs)
else:
raise ValueError("Unrecognized dtype: {}".format(self.dtype))
[docs] def move_up(self, change):
value_range = self.value[1] - self.value[0]
if self.value[1] + value_range < self.vmax:
self.value = (self.value[0] + value_range, self.value[1] + value_range)
else:
self.move_end(change)
[docs] def move_down(self, change):
value_range = self.value[1] - self.value[0]
if self.value[0] - value_range > self.vmin:
self.value = (self.value[0] - value_range, self.value[1] - value_range)
else:
self.move_start(change)
[docs] def move_start(self, change):
value_range = self.value[1] - self.value[0]
self.value = (self.vmin, self.vmin + value_range)
[docs] def move_end(self, change):
value_range = self.value[1] - self.value[0]
self.value = (self.vmax - value_range, self.vmax)
[docs] def get_children(self):
if self.orientation == "horizontal":
return [
self.slider,
self.to_start_button,
self.forward_button,
self.backwards_button,
self.to_end_button,
]
else:
return [
widgets.VBox(
[
self.slider,
self.to_end_button,
self.forward_button,
self.backwards_button,
self.to_start_button,
],
layout=widgets.Layout(
display="flex", flex_flow="column", align_items="center"
),
)
]
[docs]class StartAndDurationController(WindowController):
"""
Can be used in place of the RangeController.
"""
DEFAULT_DURATION = 5
def __init__(
self, tmax, tmin=0, start_value=None, description="start (s)", **kwargs
):
"""
Parameters
----------
tmax: float
in seconds
tmin: float
in seconds
start_value: (float, float)
start and stop in seconds
description: str
kwargs: dict
"""
if tmin > tmax:
raise ValueError(
"tmax and tmin were probably entered in the wrong order. tmax should be first"
)
super().__init__(tmin, tmax, start_value, description, **kwargs)
self.slider = widgets.FloatSlider(
value=start_value,
min=tmin,
max=tmax,
step=0.01,
description=description,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format=".2f",
style={"description_width": "initial"},
layout=Layout(width="100%", min_width="250px"),
)
link((self.slider, "description"), (self, "description"))
if start_value is None:
duration = min(self.DEFAULT_DURATION, tmax - tmin)
else:
duration = start_value[1] - start_value[0]
self.duration = widgets.BoundedFloatText(
value=duration,
min=0,
max=tmax - tmin,
step=0.1,
description="duration (s):",
style={"description_width": "initial"},
layout=Layout(max_width="200px"),
)
self.value = (self.slider.value, self.slider.value + self.duration.value)
self.slider.observe(self.monitor_slider)
self.duration.observe(self.monitor_duration)
self.children = self.get_children()
[docs] def monitor_slider(self, change):
if "new" in change:
if isinstance(change["new"], dict):
if "value" in change["new"]:
value = change["new"]["value"]
else:
return
else:
value = change["new"]
if self.slider.value + self.duration.value > self.vmax:
self.slider.value = self.vmax - self.duration.value
else:
self.value = (value, value + self.duration.value)
[docs] def monitor_duration(self, change):
if "new" in change:
if isinstance(change["new"], dict):
if "value" in change["new"]:
value = change["new"]["value"]
if self.slider.value + value > self.vmax:
self.slider.value = self.vmax - value
self.value = (self.slider.value, self.slider.value + value)
[docs] def move_up(self, change):
if self.slider.value + 2 * self.duration.value < self.vmax:
self.slider.value += self.duration.value
else:
self.move_end(change)
[docs] def move_down(self, change):
if self.slider.value - self.duration.value > self.vmin:
self.slider.value -= self.duration.value
else:
self.move_start(change)
[docs] def move_start(self, change):
self.slider.value = self.vmin
[docs] def move_end(self, change):
self.slider.value = self.vmax - self.duration.value
[docs] def get_children(self):
if self.orientation == "horizontal":
return [
self.slider,
self.duration,
# self.to_start_button,
self.backwards_button,
self.forward_button,
# self.to_end_button
]
else:
return [
widgets.VBox(
[
self.slider,
self.duration,
# self.to_end_button,
self.forward_button,
self.backwards_button,
# self.to_start_button,
],
layout=widgets.Layout(
display="flex", flex_flow="column", align_items="center"
),
)
]