"""
Layout component to lay out objects in a set of tabs.
"""
from __future__ import annotations
from collections import defaultdict
from typing import (
TYPE_CHECKING, ClassVar, List, Mapping, Type,
)
import param
from bokeh.models import Spacer as BkSpacer, TabPanel as BkTabPanel
from ..models.tabs import Tabs as BkTabs
from ..viewable import Layoutable
from .base import NamedListPanel
if TYPE_CHECKING:
from bokeh.model import Model
[docs]class Tabs(NamedListPanel):
"""
The `Tabs` layout allows switching between multiple objects by clicking
on the corresponding tab header.
Tab labels may be defined explicitly as part of a tuple or will be
inferred from the `name` parameter of the tab’s contents.
Like `Column` and `Row`, `Tabs` has a list-like API with methods to
`append`, `extend`, `clear`, `insert`, `pop`, `remove` and `__setitem__`,
which make it possible to interactively update and modify the tabs.
Reference: https://panel.holoviz.org/reference/layouts/Tabs.html
:Example:
>>> pn.Tabs(('Scatter', plot1), some_pane_with_a_name)
"""
closable = param.Boolean(default=False, doc="""
Whether it should be possible to close tabs.""")
dynamic = param.Boolean(default=False, doc="""
Dynamically populate only the active tab.""")
tabs_location = param.ObjectSelector(
default='above', objects=['above', 'below', 'left', 'right'], doc="""
The location of the tabs relative to the tab contents.""")
height = param.Integer(default=None, bounds=(0, None))
width = param.Integer(default=None, bounds=(0, None))
_bokeh_model: ClassVar[Type[Model]] = BkTabs
_direction: ClassVar[str | None] = 'vertical'
_js_transforms: ClassVar[Mapping[str, str]] = {'tabs': """
var ids = [];
for (var t of value) {{ ids.push(t.id) }};
var value = ids;
"""}
_manual_params: ClassVar[List[str]] = ['closable']
_rename: ClassVar[Mapping[str, str | None]] = {
'closable': None, 'dynamic': None, 'name': None, 'objects': 'tabs'
}
_source_transforms: ClassVar[Mapping[str, str | None]] = {
'dynamic': None, 'objects': None
}
def __init__(self, *objects, **params):
super().__init__(*objects, **params)
self._rendered = defaultdict(dict)
self.param.active.bounds = (-1, len(self)-1)
self.param.watch(self._update_active, ['dynamic', 'active'])
def _update_names(self, event):
# No active tabs will have a value of -1
self.param.active.bounds = (-1, len(event.new)-1)
super()._update_names(event)
def _cleanup(self, root: Model | None = None) -> None:
super()._cleanup(root)
if root:
if root.ref['id'] in self._panels:
del self._panels[root.ref['id']]
if root.ref['id'] in self._rendered:
del self._rendered[root.ref['id']]
@property
def _preprocess_params(self):
return NamedListPanel._preprocess_params + (['active'] if self.dynamic else [])
#----------------------------------------------------------------
# Callback API
#----------------------------------------------------------------
def _process_close(self, ref, attr, old, new):
"""
Handle closed tabs.
"""
model, _ = self._models.get(ref)
if model:
try:
inds = [old.index(tab) for tab in new]
except Exception:
return old, None
old = self.objects
new = [old[i] for i in inds]
return old, new
def _comm_change(self, doc, ref, comm, subpath, attr, old, new):
if attr in self._changing.get(ref, []):
self._changing[ref].remove(attr)
return
if attr == 'tabs':
old, new = self._process_close(ref, attr, old, new)
if new is None:
return
super()._comm_change(doc, ref, comm, subpath, attr, old, new)
def _server_change(self, doc, ref, subpath, attr, old, new):
if attr in self._changing.get(ref, []):
self._changing[ref].remove(attr)
return
if attr == 'tabs':
old, new = self._process_close(ref, attr, old, new)
if new is None:
return
super()._server_change(doc, ref, subpath, attr, old, new)
def _update_active(self, *events):
for event in events:
if event.name == 'dynamic' or (self.dynamic and event.name == 'active'):
self.param.trigger('objects')
return
def _compute_sizing_mode(self, children, props):
children = [child.child for child in children]
return super()._compute_sizing_mode(children, props)
#----------------------------------------------------------------
# Model API
#----------------------------------------------------------------
def _manual_update(self, events, model, doc, root, parent, comm):
for event in events:
if event.name == 'closable':
for child in model.tabs:
child.closable = event.new
def _get_objects(self, model, old_objects, doc, root, comm=None):
"""
Returns new child models for the layout while reusing unchanged
models and cleaning up any dropped objects.
"""
from ..pane.base import RerenderError, panel
new_models, old_models = [], []
if len(self._names) != len(self):
raise ValueError('Tab names do not match objects, ensure '
'that the Tabs.objects are not modified '
'directly. Found %d names, expected %d.' %
(len(self._names), len(self)))
for i, (name, pane) in enumerate(zip(self._names, self)):
pane = panel(pane, name=name)
self.objects[i] = pane
ref = root.ref['id']
panels = self._panels[ref]
rendered = self._rendered[ref]
for obj in old_objects:
if obj in self.objects:
continue
obj._cleanup(root)
panels.pop(id(obj), None)
rendered.pop(id(obj), None)
current_objects = list(self)
for i, (name, pane) in enumerate(zip(self._names, self)):
pref = id(pane)
hidden = self.dynamic and i != self.active
panel = panels.get(pref)
prev_hidden = (
hasattr(panel, 'child') and isinstance(panel.child, BkSpacer) and
panel.child.tags == ['hidden']
)
# If object has not changed, we have not toggled between
# hidden and unhidden state or the tabs are not
# dynamic then reuse the panel
if (pane in old_objects and pref in panels and
(not (hidden ^ prev_hidden) or not (self.dynamic or prev_hidden))):
new_models.append(panel)
continue
if prev_hidden and not hidden and pref in rendered:
child = rendered[pref]
old_models.append(child)
elif hidden:
child = BkSpacer(**{k: v for k, v in pane.param.values().items()
if k in Layoutable.param and v is not None and
k not in ('name', 'design')})
child.tags = ['hidden']
else:
try:
rendered[pref] = child = pane._get_model(doc, root, model, comm)
except RerenderError as e:
if e.layout is not None and e.layout is not self:
raise e
e.layout = None
return self._get_objects(model, current_objects[:i], doc, root, comm)
panel = panels[pref] = BkTabPanel(
title=name, name=pane.name, child=child, closable=self.closable
)
new_models.append(panel)
return new_models, old_models