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') 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 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 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 attr_map={} focus_map={'a': 'b'}> >>> w.set_focus_map(None) >>> w 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