"""Custom widgets that are nor input nor UI interface.
Gather the customized ``ipyvuetifyWidgets``. All the content of this modules is included in the parent ``sepal_ui.sepalwidgets`` package. So it can be imported directly from there.
Example:
.. jupyter-execute::
from sepal_ui import sepalwidgets as sw
sw.CopyToClip()
"""
from pathlib import Path
from typing import Optional
import ipyvuetify as v
import traitlets as t
from deprecated.sphinx import versionadded
from markdown import markdown
from traitlets import link, observe
from sepal_ui import color
from sepal_ui.model import Model
from sepal_ui.sepalwidgets.sepalwidget import SepalWidget, Tooltip
__all__ = ["Markdown", "CopyToClip", "StateIcon"]
[docs]
class Markdown(v.Layout, SepalWidget):
[docs]
def __init__(self, mkd_str: str = "", **kwargs) -> None:
"""Custom Layout based on the markdown text given.
Args:
mkd_str: the text to display using the markdown convention. multi-line string are also interpreted
kwargs: Any parameter from a v.Layout. If set, 'children' will be overwritten
"""
mkd = markdown(mkd_str, extensions=["fenced_code", "sane_lists"])
# need to be nested in a div to be displayed
mkd = "<div>\n" + mkd + "\n</div>"
# make every link to point to target black (to avoid nested iframe in sepal)
mkd = mkd.replace("<a", '<a target="_blank"')
# create a Html widget
class MyHTML(v.VuetifyTemplate):
template = t.Unicode(mkd).tag(sync=True)
content = MyHTML()
# set default parameters
kwargs.setdefault("row", True)
kwargs.setdefault("class_", "pa-5")
kwargs.setdefault("align_center", True)
kwargs["children"] = [v.Flex(xs12=True, children=[content])]
# call the constructor
super().__init__(**kwargs)
[docs]
@versionadded(version="2.2.0", reason="New clipping widget")
class CopyToClip(v.VuetifyTemplate):
tf: Optional[v.TextField] = None
"v.TextField: the textfield widget that holds the v_model to copy"
v_model: t.Unicode = t.Unicode("").tag(sync=True)
"a v_model trait that embed the string to copy"
[docs]
def __init__(self, **kwargs) -> None:
"""Custom textField that provides a handy copy-to-clipboard javascript behaviour.
When the clipboard btn is clicked the v_model will be copied in the local browser clipboard. You just have to change the clipboard v_model. when copied, the icon change from a copy to a check.
Args:
kwargs: any argument that can be used with a v.TextField
"""
# add the default params to kwargs
kwargs.setdefault("outlined", True)
kwargs.setdefault("label", "Copy To clipboard")
kwargs.setdefault("readonly", True)
kwargs.setdefault("append_icon", "fa-solid fa-clipboard")
kwargs.setdefault("v_model", "")
kwargs.setdefault("class_", "ma-5")
# set the default v_model
self.v_model = kwargs["v_model"]
# create component
self.tf = v.TextField(**kwargs)
self.components = {"mytf": self.tf}
# template with js behaviour
js_dir = Path(__file__).parents[1] / "frontend/js"
clip = (js_dir / "jupyter_clip.js").read_text()
self.template = (
"<mytf/>" "<script>{methods: {jupyter_clip(_txt) {%s}}}</script>" % clip
)
super().__init__()
# js behaviour
self.tf.on_event("click:append", self._clip)
link((self, "v_model"), (self.tf, "v_model"))
def _clip(self, *args) -> None:
"""Launch the javascript clipping process."""
self.send({"method": "clip", "args": [self.tf.v_model]})
self.tf.append_icon = "fa-solid fa-clipboard-check"
return
[docs]
class StateIcon(Tooltip):
values: t.Any = t.Any().tag(sync=True)
"bool, str, int: key name of the current state of component. Values must be same as states_dict keys."
states: dict = {}
'Dictionary where keys are the state name to be linked with self value and value represented by a tuple of two elements. {"key":(tooltip_msg, color)}.'
icon: Optional[v.Icon]
"The colored Icon of the tooltip"
[docs]
def __init__(
self,
model: Optional[Model] = None,
model_trait: str = "",
states: dict = {},
**kwargs,
):
"""Custom icon with multiple state colors.
Args:
model: Model to manage StateIcon behaviour from outside.
model_trait: Name of trait to be linked with state icon. Must exists in model.
states: Dictionary where keys are the state name to be linked with self value and value represented by a tuple of two elements. {"key":(tooltip_msg, color)}.
kwargs: Any arguments from a v.Tooltip
"""
# set the default parameter of the tooltip
kwargs.setdefault("right", True)
# init the states
default_states = {
"valid": ("Valid", color.success),
"non_valid": ("Not valid", color.error),
}
self.states = default_states if not states else states
# Get the first value (states first key) to use as default one
init_value = self.states[next(iter(self.states))]
self.icon = v.Icon(
children=["fa-solid fa-circle"], color=init_value[1], small=True
)
super().__init__(self.icon, init_value[0], **kwargs)
# Directional from there to link here.
if all([model, model_trait]):
link((model, model_trait), (self, "values"))
@observe("values")
def _swap(self, change: dict) -> None:
"""Swap between states."""
new_val = change["new"]
# Use the first value when there is not initial value.
if not new_val:
self.value = next(iter(self.states))
return
# Perform a little check with comprehensive error message
if new_val not in self.states:
raise ValueError(
f"Value '{new_val}' is not a valid value. Use {list(self.states.keys())}"
)
self.icon.color = self.states[new_val][1]
self.children = [self.states[new_val][0]]
return