162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import typing
|
|
from collections.abc import Hashable, Mapping
|
|
|
|
from urwid.canvas import CompositeCanvas
|
|
|
|
from .widget import WidgetError, delegate_to_widget_mixin
|
|
from .widget_decoration import WidgetDecoration
|
|
|
|
WrappedWidget = typing.TypeVar("WrappedWidget")
|
|
|
|
|
|
class AttrMapError(WidgetError):
|
|
pass
|
|
|
|
|
|
class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[WrappedWidget]):
|
|
"""
|
|
AttrMap is a decoration that maps one set of attributes to another.
|
|
This object will pass all function calls and variable references to the
|
|
wrapped widget.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
w: WrappedWidget,
|
|
attr_map: Hashable | Mapping[Hashable | None, Hashable] | None,
|
|
focus_map: Hashable | Mapping[Hashable | None, Hashable] | None = None,
|
|
) -> None:
|
|
"""
|
|
:param w: widget to wrap (stored as self.original_widget)
|
|
:type w: widget
|
|
|
|
:param attr_map: attribute to apply to *w*, or dict of old display
|
|
attribute: new display attribute mappings
|
|
:type attr_map: display attribute or dict
|
|
|
|
:param focus_map: attribute to apply when in focus or dict of
|
|
old display attribute: new display attribute mappings;
|
|
if ``None`` use *attr*
|
|
:type focus_map: display attribute or dict
|
|
|
|
>>> from urwid import Divider, Edit, Text
|
|
>>> AttrMap(Divider(u"!"), 'bright')
|
|
<AttrMap flow widget <Divider flow widget '!'> attr_map={None: 'bright'}>
|
|
>>> AttrMap(Edit(), 'notfocus', 'focus').attr_map
|
|
{None: 'notfocus'}
|
|
>>> AttrMap(Edit(), 'notfocus', 'focus').focus_map
|
|
{None: 'focus'}
|
|
>>> size = (5,)
|
|
>>> am = AttrMap(Text(u"hi"), 'greeting', 'fgreet')
|
|
>>> next(am.render(size, focus=False).content()) # ... = b in Python 3
|
|
[('greeting', None, ...'hi ')]
|
|
>>> next(am.render(size, focus=True).content())
|
|
[('fgreet', None, ...'hi ')]
|
|
>>> am2 = AttrMap(Text(('word', u"hi")), {'word':'greeting', None:'bg'})
|
|
>>> am2
|
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'word': 'greeting', None: 'bg'}>
|
|
>>> next(am2.render(size).content())
|
|
[('greeting', None, ...'hi'), ('bg', None, ...' ')]
|
|
"""
|
|
super().__init__(w)
|
|
|
|
if isinstance(attr_map, Mapping):
|
|
self.attr_map = dict(attr_map)
|
|
else:
|
|
self.attr_map = {None: attr_map}
|
|
|
|
if isinstance(focus_map, Mapping):
|
|
self.focus_map = dict(focus_map)
|
|
elif focus_map is None:
|
|
self.focus_map = focus_map
|
|
else:
|
|
self.focus_map = {None: focus_map}
|
|
|
|
def _repr_attrs(self) -> dict[str, typing.Any]:
|
|
# only include the focus_attr when it takes effect (not None)
|
|
d = {**super()._repr_attrs(), "attr_map": self._attr_map}
|
|
if self._focus_map is not None:
|
|
d["focus_map"] = self._focus_map
|
|
return d
|
|
|
|
def get_attr_map(self) -> dict[Hashable | None, Hashable]:
|
|
# make a copy so ours is not accidentally modified
|
|
# FIXME: a dictionary that detects modifications would be better
|
|
return dict(self._attr_map)
|
|
|
|
def set_attr_map(self, attr_map: dict[Hashable | None, Hashable] | None) -> None:
|
|
"""
|
|
Set the attribute mapping dictionary {from_attr: to_attr, ...}
|
|
|
|
Note this function does not accept a single attribute the way the
|
|
constructor does. You must specify {None: attribute} instead.
|
|
|
|
>>> from urwid import Text
|
|
>>> w = AttrMap(Text(u"hi"), None)
|
|
>>> w.set_attr_map({'a':'b'})
|
|
>>> w
|
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'a': 'b'}>
|
|
"""
|
|
for from_attr, to_attr in attr_map.items():
|
|
if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
|
|
raise AttrMapError(
|
|
f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
|
|
)
|
|
|
|
self._attr_map = attr_map
|
|
self._invalidate()
|
|
|
|
attr_map = property(get_attr_map, set_attr_map)
|
|
|
|
def get_focus_map(self) -> dict[Hashable | None, Hashable] | None:
|
|
# make a copy so ours is not accidentally modified
|
|
# FIXME: a dictionary that detects modifications would be better
|
|
if self._focus_map:
|
|
return dict(self._focus_map)
|
|
return None
|
|
|
|
def set_focus_map(self, focus_map: dict[Hashable | None, Hashable] | None) -> None:
|
|
"""
|
|
Set the focus attribute mapping dictionary
|
|
{from_attr: to_attr, ...}
|
|
|
|
If None this widget will use the attr mapping instead (no change
|
|
when in focus).
|
|
|
|
Note this function does not accept a single attribute the way the
|
|
constructor does. You must specify {None: attribute} instead.
|
|
|
|
>>> from urwid import Text
|
|
>>> w = AttrMap(Text(u"hi"), {})
|
|
>>> w.set_focus_map({'a':'b'})
|
|
>>> w
|
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={'a': 'b'}>
|
|
>>> w.set_focus_map(None)
|
|
>>> w
|
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={}>
|
|
"""
|
|
if focus_map is not None:
|
|
for from_attr, to_attr in focus_map.items():
|
|
if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
|
|
raise AttrMapError(
|
|
f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
|
|
)
|
|
self._focus_map = focus_map
|
|
self._invalidate()
|
|
|
|
focus_map = property(get_focus_map, set_focus_map)
|
|
|
|
def render(self, size, focus: bool = False) -> CompositeCanvas:
|
|
"""
|
|
Render wrapped widget and apply attribute. Return canvas.
|
|
"""
|
|
attr_map = self._attr_map
|
|
if focus and self._focus_map is not None:
|
|
attr_map = self._focus_map
|
|
canv = self._original_widget.render(size, focus=focus)
|
|
canv = CompositeCanvas(canv)
|
|
canv.fill_attr_apply(attr_map)
|
|
return canv
|