hub/venv/lib/python3.7/site-packages/ipywidgets/widgets/widget_templates.py

455 lines
15 KiB
Python

"""Implement common widgets layouts as reusable components"""
import re
from collections import defaultdict
from traitlets import Instance, Bool, Unicode, CUnicode, CaselessStrEnum, Tuple
from traitlets import Integer
from traitlets import HasTraits, TraitError
from traitlets import observe, validate
from .widget import Widget
from .widget_box import GridBox
from .docutils import doc_subst
_doc_snippets = {
'style_params' : """
grid_gap : str
CSS attribute used to set the gap between the grid cells
justify_content : str, in ['flex-start', 'flex-end', 'center', 'space-between', 'space-around']
CSS attribute used to align widgets vertically
align_items : str, in ['top', 'bottom', 'center', 'flex-start', 'flex-end', 'baseline', 'stretch']
CSS attribute used to align widgets horizontally
width : str
height : str
width and height"""
}
@doc_subst(_doc_snippets)
class LayoutProperties(HasTraits):
"""Mixin class for layout templates
This class handles mainly style attributes (height, grid_gap etc.)
Parameters
----------
{style_params}
Note
----
This class is only meant to be used in inheritance as mixin with other
classes. It will not work, unless `self.layout` attribute is defined.
"""
# style attributes (passed to Layout)
grid_gap = Unicode(
None,
allow_none=True,
help="The grid-gap CSS attribute.")
justify_content = CaselessStrEnum(
['flex-start', 'flex-end', 'center',
'space-between', 'space-around'],
allow_none=True,
help="The justify-content CSS attribute.")
align_items = CaselessStrEnum(
['top', 'bottom',
'flex-start', 'flex-end', 'center',
'baseline', 'stretch'],
allow_none=True, help="The align-items CSS attribute.")
width = Unicode(
None,
allow_none=True,
help="The width CSS attribute.")
height = Unicode(
None,
allow_none=True,
help="The width CSS attribute.")
def __init__(self, **kwargs):
super(LayoutProperties, self).__init__(**kwargs)
self._property_rewrite = defaultdict(dict)
self._property_rewrite['align_items'] = {'top': 'flex-start',
'bottom': 'flex-end'}
self._copy_layout_props()
self._set_observers()
def _delegate_to_layout(self, change):
"delegate the trait types to their counterparts in self.layout"
value, name = change['new'], change['name']
value = self._property_rewrite[name].get(value, value)
setattr(self.layout, name, value) # pylint: disable=no-member
def _set_observers(self):
"set observers on all layout properties defined in this class"
_props = LayoutProperties.class_trait_names()
self.observe(self._delegate_to_layout, _props)
def _copy_layout_props(self):
_props = LayoutProperties.class_trait_names()
for prop in _props:
value = getattr(self, prop)
if value:
value = self._property_rewrite[prop].get(value, value)
setattr(self.layout, prop, value) #pylint: disable=no-member
@doc_subst(_doc_snippets)
class AppLayout(GridBox, LayoutProperties):
""" Define an application like layout of widgets.
Parameters
----------
header: instance of Widget
left_sidebar: instance of Widget
center: instance of Widget
right_sidebar: instance of Widget
footer: instance of Widget
widgets to fill the positions in the layout
merge: bool
flag to say whether the empty positions should be automatically merged
pane_widths: list of numbers/strings
the fraction of the total layout width each of the central panes should occupy
(left_sidebar,
center, right_sidebar)
pane_heights: list of numbers/strings
the fraction of the width the vertical space that the panes should occupy
(left_sidebar, center, right_sidebar)
{style_params}
Examples
--------
"""
# widget positions
header = Instance(Widget, allow_none=True)
footer = Instance(Widget, allow_none=True)
left_sidebar = Instance(Widget, allow_none=True)
right_sidebar = Instance(Widget, allow_none=True)
center = Instance(Widget, allow_none=True)
# extra args
pane_widths = Tuple(CUnicode(), CUnicode(), CUnicode(),
default_value=['1fr', '2fr', '1fr'])
pane_heights = Tuple(CUnicode(), CUnicode(), CUnicode(),
default_value=['1fr', '3fr', '1fr'])
merge = Bool(default_value=True)
def __init__(self, **kwargs):
super(AppLayout, self).__init__(**kwargs)
self._update_layout()
@staticmethod
def _size_to_css(size):
if re.match(r'\d+\.?\d*(px|fr|%)$', size):
return size
if re.match(r'\d+\.?\d*$', size):
return size + 'fr'
raise TypeError("the pane sizes must be in one of the following formats: "
"'10px', '10fr', 10 (will be converted to '10fr')."
"Got '{}'".format(size))
def _convert_sizes(self, size_list):
return list(map(self._size_to_css, size_list))
def _update_layout(self):
grid_template_areas = [["header", "header", "header"],
["left-sidebar", "center", "right-sidebar"],
["footer", "footer", "footer"]]
grid_template_columns = self._convert_sizes(self.pane_widths)
grid_template_rows = self._convert_sizes(self.pane_heights)
all_children = {'header': self.header,
'footer': self.footer,
'left-sidebar': self.left_sidebar,
'right-sidebar': self.right_sidebar,
'center': self.center}
children = {position : child
for position, child in all_children.items()
if child is not None}
if not children:
return
for position, child in children.items():
child.layout.grid_area = position
if self.merge:
if len(children) == 1:
position = list(children.keys())[0]
grid_template_areas = [[position, position, position],
[position, position, position],
[position, position, position]]
else:
if self.center is None:
for row in grid_template_areas:
del row[1]
del grid_template_columns[1]
if self.left_sidebar is None:
grid_template_areas[1][0] = grid_template_areas[1][1]
if self.right_sidebar is None:
grid_template_areas[1][-1] = grid_template_areas[1][-2]
if (self.left_sidebar is None and
self.right_sidebar is None and
self.center is None):
grid_template_areas = [['header'], ['footer']]
grid_template_columns = ['1fr']
grid_template_rows = ['1fr', '1fr']
if self.header is None:
del grid_template_areas[0]
del grid_template_rows[0]
if self.footer is None:
del grid_template_areas[-1]
del grid_template_rows[-1]
grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
for line in grid_template_areas)
self.layout.grid_template_columns = " ".join(grid_template_columns)
self.layout.grid_template_rows = " ".join(grid_template_rows)
self.layout.grid_template_areas = grid_template_areas_css
self.children = tuple(children.values())
@observe("footer", "header", "center", "left_sidebar", "right_sidebar", "merge",
"pane_widths", "pane_heights")
def _child_changed(self, change): #pylint: disable=unused-argument
self._update_layout()
@doc_subst(_doc_snippets)
class GridspecLayout(GridBox, LayoutProperties):
""" Define a N by M grid layout
Parameters
----------
n_rows : int
number of rows in the grid
n_columns : int
number of columns in the grid
{style_params}
Examples
--------
>>> from ipywidgets import GridspecLayout, Button, Layout
>>> layout = GridspecLayout(n_rows=4, n_columns=2, height='200px')
>>> layout[:3, 0] = Button(layout=Layout(height='auto', width='auto'))
>>> layout[1:, 1] = Button(layout=Layout(height='auto', width='auto'))
>>> layout[-1, 0] = Button(layout=Layout(height='auto', width='auto'))
>>> layout[0, 1] = Button(layout=Layout(height='auto', width='auto'))
>>> layout
"""
n_rows = Integer()
n_columns = Integer()
def __init__(self, n_rows=None, n_columns=None, **kwargs):
super(GridspecLayout, self).__init__(**kwargs)
self.n_rows = n_rows
self.n_columns = n_columns
self._grid_template_areas = [['.'] * self.n_columns for i in range(self.n_rows)]
self._grid_template_rows = 'repeat(%d, 1fr)' % (self.n_rows,)
self._grid_template_columns = 'repeat(%d, 1fr)' % (self.n_columns,)
self._children = {}
self._id_count = 0
@validate('n_rows', 'n_columns')
def _validate_integer(self, proposal):
if proposal['value'] > 0:
return proposal['value']
raise TraitError('n_rows and n_columns must be positive integer')
def _get_indices_from_slice(self, row, column):
"convert a two-dimensional slice to a list of rows and column indices"
if isinstance(row, slice):
start, stop, stride = row.indices(self.n_rows)
rows = range(start, stop, stride)
else:
rows = [row]
if isinstance(column, slice):
start, stop, stride = column.indices(self.n_columns)
columns = range(start, stop, stride)
else:
columns = [column]
return rows, columns
def __setitem__(self, key, value):
row, column = key
self._id_count += 1
obj_id = 'widget%03d' % self._id_count
value.layout.grid_area = obj_id
rows, columns = self._get_indices_from_slice(row, column)
for row in rows:
for column in columns:
current_value = self._grid_template_areas[row][column]
if current_value != '.' and current_value in self._children:
del self._children[current_value]
self._grid_template_areas[row][column] = obj_id
self._children[obj_id] = value
self._update_layout()
def __getitem__(self, key):
rows, columns = self._get_indices_from_slice(*key)
obj_id = None
for row in rows:
for column in columns:
new_obj_id = self._grid_template_areas[row][column]
obj_id = obj_id or new_obj_id
if obj_id != new_obj_id:
raise TypeError('The slice spans several widgets, but '
'only a single widget can be retrieved '
'at a time')
return self._children[obj_id]
def _update_layout(self):
grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
for line in self._grid_template_areas)
self.layout.grid_template_columns = self._grid_template_columns
self.layout.grid_template_rows = self._grid_template_rows
self.layout.grid_template_areas = grid_template_areas_css
self.children = tuple(self._children.values())
@doc_subst(_doc_snippets)
class TwoByTwoLayout(GridBox, LayoutProperties):
""" Define a layout with 2x2 regular grid.
Parameters
----------
top_left: instance of Widget
top_right: instance of Widget
bottom_left: instance of Widget
bottom_right: instance of Widget
widgets to fill the positions in the layout
merge: bool
flag to say whether the empty positions should be automatically merged
{style_params}
Examples
--------
>>> from ipywidgets import TwoByTwoLayout, Button
>>> TwoByTwoLayout(top_left=Button(description="Top left"),
... top_right=Button(description="Top right"),
... bottom_left=Button(description="Bottom left"),
... bottom_right=Button(description="Bottom right"))
"""
# widget positions
top_left = Instance(Widget, allow_none=True)
top_right = Instance(Widget, allow_none=True)
bottom_left = Instance(Widget, allow_none=True)
bottom_right = Instance(Widget, allow_none=True)
# extra args
merge = Bool(default_value=True)
def __init__(self, **kwargs):
super(TwoByTwoLayout, self).__init__(**kwargs)
self._update_layout()
def _update_layout(self):
grid_template_areas = [["top-left", "top-right"],
["bottom-left", "bottom-right"]]
all_children = {'top-left' : self.top_left,
'top-right' : self.top_right,
'bottom-left' : self.bottom_left,
'bottom-right' : self.bottom_right}
children = {position : child
for position, child in all_children.items()
if child is not None}
if not children:
return
for position, child in children.items():
child.layout.grid_area = position
if self.merge:
if len(children) == 1:
position = list(children.keys())[0]
grid_template_areas = [[position, position],
[position, position]]
else:
columns = ['left', 'right']
for i, column in enumerate(columns):
top, bottom = children.get('top-' + column), children.get('bottom-' + column)
i_neighbour = (i + 1) % 2
if top is None and bottom is None:
# merge each cell in this column with the neighbour on the same row
grid_template_areas[0][i] = grid_template_areas[0][i_neighbour]
grid_template_areas[1][i] = grid_template_areas[1][i_neighbour]
elif top is None:
# merge with the cell below
grid_template_areas[0][i] = grid_template_areas[1][i]
elif bottom is None:
# merge with the cell above
grid_template_areas[1][i] = grid_template_areas[0][i]
grid_template_areas_css = "\n".join('"{}"'.format(" ".join(line))
for line in grid_template_areas)
self.layout.grid_template_columns = '1fr 1fr'
self.layout.grid_template_rows = '1fr 1fr'
self.layout.grid_template_areas = grid_template_areas_css
self.children = tuple(children.values())
@observe("top_left", "bottom_left", "top_right", "bottom_right", "merge")
def _child_changed(self, change): #pylint: disable=unused-argument
self._update_layout()