Automated update
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from urwid.canvas import CanvasCombine, CompositeCanvas
|
||||
from urwid.split_repr import remove_defaults
|
||||
from urwid.util import is_mouse_press
|
||||
|
||||
from .constants import Sizing, VAlign
|
||||
from .container import WidgetContainerMixin
|
||||
from .filler import Filler
|
||||
from .widget import Widget, WidgetError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Iterator, MutableMapping
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
BodyWidget = typing.TypeVar("BodyWidget")
|
||||
HeaderWidget = typing.TypeVar("HeaderWidget")
|
||||
FooterWidget = typing.TypeVar("FooterWidget")
|
||||
|
||||
|
||||
class FrameError(WidgetError):
|
||||
pass
|
||||
|
||||
|
||||
def _check_widget_subclass(widget: Widget | None) -> None:
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if not isinstance(widget, Widget):
|
||||
obj_class_path = f"{widget.__class__.__module__}.{widget.__class__.__name__}"
|
||||
warnings.warn(
|
||||
f"{obj_class_path} is not subclass of Widget",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
class Frame(Widget, WidgetContainerMixin, typing.Generic[BodyWidget, HeaderWidget, FooterWidget]):
|
||||
"""
|
||||
Frame widget is a box widget with optional header and footer
|
||||
flow widgets placed above and below the box widget.
|
||||
|
||||
.. note:: The main difference between a Frame and a :class:`Pile` widget
|
||||
defined as: `Pile([('pack', header), body, ('pack', footer)])` is that
|
||||
the Frame will not automatically change focus up and down in response to keystrokes.
|
||||
"""
|
||||
|
||||
_selectable = True
|
||||
_sizing = frozenset([Sizing.BOX])
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
body: BodyWidget,
|
||||
header: HeaderWidget = None,
|
||||
footer: FooterWidget = None,
|
||||
focus_part: Literal["header", "footer", "body"] | Widget = "body",
|
||||
):
|
||||
"""
|
||||
:param body: a box widget for the body of the frame
|
||||
:type body: Widget
|
||||
:param header: a flow widget for above the body (or None)
|
||||
:type header: Widget
|
||||
:param footer: a flow widget for below the body (or None)
|
||||
:type footer: Widget
|
||||
:param focus_part: 'header', 'footer' or 'body'
|
||||
:type focus_part: str | Widget
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self._header = header
|
||||
self._body = body
|
||||
self._footer = footer
|
||||
if focus_part in {"header", "footer", "body"}:
|
||||
self.focus_part = focus_part
|
||||
elif focus_part == header:
|
||||
self.focus_part = "header"
|
||||
elif focus_part == footer:
|
||||
self.focus_part = "footer"
|
||||
elif focus_part == body:
|
||||
self.focus_part = "body"
|
||||
else:
|
||||
raise ValueError(f"Invalid focus part {focus_part!r}")
|
||||
|
||||
_check_widget_subclass(header)
|
||||
_check_widget_subclass(body)
|
||||
_check_widget_subclass(footer)
|
||||
|
||||
def _repr_attrs(self) -> dict[str, typing.Any]:
|
||||
attrs = {
|
||||
**super()._repr_attrs(),
|
||||
"body": self._body,
|
||||
"header": self._header,
|
||||
"footer": self._footer,
|
||||
"focus_part": self.focus_part,
|
||||
}
|
||||
return remove_defaults(attrs, Frame.__init__)
|
||||
|
||||
def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
|
||||
yield "body", self._body
|
||||
yield "header", self._header
|
||||
yield "footer", self._footer
|
||||
yield "focus_part", self.focus_part
|
||||
|
||||
@property
|
||||
def header(self) -> HeaderWidget:
|
||||
return self._header
|
||||
|
||||
@header.setter
|
||||
def header(self, header: HeaderWidget) -> None:
|
||||
_check_widget_subclass(header)
|
||||
self._header = header
|
||||
if header is None and self.focus_part == "header":
|
||||
self.focus_part = "body"
|
||||
self._invalidate()
|
||||
|
||||
def get_header(self) -> HeaderWidget:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.get_header` is deprecated, "
|
||||
f"standard property `{self.__class__.__name__}.header` should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.header
|
||||
|
||||
def set_header(self, header: HeaderWidget) -> None:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.set_header` is deprecated, "
|
||||
f"standard property `{self.__class__.__name__}.header` should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.header = header
|
||||
|
||||
@property
|
||||
def body(self) -> BodyWidget:
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, body: BodyWidget) -> None:
|
||||
_check_widget_subclass(body)
|
||||
self._body = body
|
||||
self._invalidate()
|
||||
|
||||
def get_body(self) -> BodyWidget:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.get_body` is deprecated, "
|
||||
f"standard property {self.__class__.__name__}.body should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.body
|
||||
|
||||
def set_body(self, body: BodyWidget) -> None:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.set_body` is deprecated, "
|
||||
f"standard property `{self.__class__.__name__}.body` should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.body = body
|
||||
|
||||
@property
|
||||
def footer(self) -> FooterWidget:
|
||||
return self._footer
|
||||
|
||||
@footer.setter
|
||||
def footer(self, footer: FooterWidget) -> None:
|
||||
_check_widget_subclass(footer)
|
||||
self._footer = footer
|
||||
if footer is None and self.focus_part == "footer":
|
||||
self.focus_part = "body"
|
||||
self._invalidate()
|
||||
|
||||
def get_footer(self) -> FooterWidget:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.get_footer` is deprecated, "
|
||||
f"standard property `{self.__class__.__name__}.footer` should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.footer
|
||||
|
||||
def set_footer(self, footer: FooterWidget) -> None:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}.set_footer` is deprecated, "
|
||||
f"standard property `{self.__class__.__name__}.footer` should be used instead",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.footer = footer
|
||||
|
||||
@property
|
||||
def focus_position(self) -> Literal["header", "footer", "body"]:
|
||||
"""
|
||||
writeable property containing an indicator which part of the frame
|
||||
that is in focus: `'body', 'header'` or `'footer'`.
|
||||
|
||||
:returns: one of 'header', 'footer' or 'body'.
|
||||
:rtype: str
|
||||
"""
|
||||
return self.focus_part
|
||||
|
||||
@focus_position.setter
|
||||
def focus_position(self, part: Literal["header", "footer", "body"]) -> None:
|
||||
"""
|
||||
Determine which part of the frame is in focus.
|
||||
|
||||
:param part: 'header', 'footer' or 'body'
|
||||
:type part: str
|
||||
"""
|
||||
if part not in {"header", "footer", "body"}:
|
||||
raise IndexError(f"Invalid position for Frame: {part}")
|
||||
if (part == "header" and self._header is None) or (part == "footer" and self._footer is None):
|
||||
raise IndexError(f"This Frame has no {part}")
|
||||
self.focus_part = part
|
||||
self._invalidate()
|
||||
|
||||
def get_focus(self) -> Literal["header", "footer", "body"]:
|
||||
"""
|
||||
writeable property containing an indicator which part of the frame
|
||||
that is in focus: `'body', 'header'` or `'footer'`.
|
||||
|
||||
.. note:: included for backwards compatibility. You should rather use
|
||||
the container property :attr:`.focus_position` to get this value.
|
||||
|
||||
:returns: one of 'header', 'footer' or 'body'.
|
||||
:rtype: str
|
||||
"""
|
||||
warnings.warn(
|
||||
"included for backwards compatibility."
|
||||
"You should rather use the container property `.focus_position` to get this value.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.focus_position
|
||||
|
||||
def set_focus(self, part: Literal["header", "footer", "body"]) -> None:
|
||||
warnings.warn(
|
||||
"included for backwards compatibility."
|
||||
"You should rather use the container property `.focus_position` to set this value.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.focus_position = part
|
||||
|
||||
@property
|
||||
def focus(self) -> BodyWidget | HeaderWidget | FooterWidget:
|
||||
"""
|
||||
child :class:`Widget` in focus: the body, header or footer widget.
|
||||
This is a read-only property."""
|
||||
return {"header": self._header, "footer": self._footer, "body": self._body}[self.focus_part]
|
||||
|
||||
def _get_focus(self) -> BodyWidget | HeaderWidget | FooterWidget:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}._get_focus` is deprecated, "
|
||||
f"please use `{self.__class__.__name__}.focus` property",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return {"header": self._header, "footer": self._footer, "body": self._body}[self.focus_part]
|
||||
|
||||
@property
|
||||
def contents(
|
||||
self,
|
||||
) -> MutableMapping[
|
||||
Literal["header", "footer", "body"],
|
||||
tuple[BodyWidget | HeaderWidget | FooterWidget, None],
|
||||
]:
|
||||
"""
|
||||
a dict-like object similar to::
|
||||
|
||||
{
|
||||
'body': (body_widget, None),
|
||||
'header': (header_widget, None), # if frame has a header
|
||||
'footer': (footer_widget, None) # if frame has a footer
|
||||
}
|
||||
|
||||
This object may be used to read or update the contents of the Frame.
|
||||
|
||||
The values are similar to the list-like .contents objects used
|
||||
in other containers with (:class:`Widget`, options) tuples, but are
|
||||
constrained to keys for each of the three usual parts of a Frame.
|
||||
When other keys are used a :exc:`KeyError` will be raised.
|
||||
|
||||
Currently, all options are `None`, but using the :meth:`options` method
|
||||
to create the options value is recommended for forwards
|
||||
compatibility.
|
||||
"""
|
||||
|
||||
# noinspection PyMethodParameters
|
||||
class FrameContents(
|
||||
typing.MutableMapping[
|
||||
str,
|
||||
typing.Tuple[typing.Union[BodyWidget, HeaderWidget, FooterWidget], None],
|
||||
]
|
||||
):
|
||||
# pylint: disable=no-self-argument
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __len__(inner_self) -> int:
|
||||
return len(inner_self.keys())
|
||||
|
||||
__getitem__ = self._contents__getitem__
|
||||
__setitem__ = self._contents__setitem__
|
||||
__delitem__ = self._contents__delitem__
|
||||
|
||||
def __iter__(inner_self) -> Iterator[str]:
|
||||
yield from inner_self.keys()
|
||||
|
||||
def __repr__(inner_self) -> str:
|
||||
return f"<{inner_self.__class__.__name__}({dict(inner_self)}) for {self}>"
|
||||
|
||||
def __rich_repr__(inner_self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
|
||||
yield from inner_self.items()
|
||||
|
||||
return FrameContents()
|
||||
|
||||
def _contents_keys(self) -> list[Literal["header", "footer", "body"]]:
|
||||
keys = ["body"]
|
||||
if self._header:
|
||||
keys.append("header")
|
||||
if self._footer:
|
||||
keys.append("footer")
|
||||
return keys
|
||||
|
||||
@typing.overload
|
||||
def _contents__getitem__(self, key: Literal["body"]) -> tuple[BodyWidget, None]: ...
|
||||
|
||||
@typing.overload
|
||||
def _contents__getitem__(self, key: Literal["header"]) -> tuple[HeaderWidget, None]: ...
|
||||
|
||||
@typing.overload
|
||||
def _contents__getitem__(self, key: Literal["footer"]) -> tuple[FooterWidget, None]: ...
|
||||
|
||||
def _contents__getitem__(
|
||||
self, key: Literal["body", "header", "footer"]
|
||||
) -> tuple[BodyWidget | HeaderWidget | FooterWidget, None]:
|
||||
if key == "body":
|
||||
return (self._body, None)
|
||||
if key == "header" and self._header:
|
||||
return (self._header, None)
|
||||
if key == "footer" and self._footer:
|
||||
return (self._footer, None)
|
||||
raise KeyError(f"Frame.contents has no key: {key!r}")
|
||||
|
||||
@typing.overload
|
||||
def _contents__setitem__(self, key: Literal["body"], value: tuple[BodyWidget, None]) -> None: ...
|
||||
|
||||
@typing.overload
|
||||
def _contents__setitem__(self, key: Literal["header"], value: tuple[HeaderWidget, None]) -> None: ...
|
||||
|
||||
@typing.overload
|
||||
def _contents__setitem__(self, key: Literal["footer"], value: tuple[FooterWidget, None]) -> None: ...
|
||||
|
||||
def _contents__setitem__(
|
||||
self,
|
||||
key: Literal["body", "header", "footer"],
|
||||
value: tuple[BodyWidget | HeaderWidget | FooterWidget, None],
|
||||
) -> None:
|
||||
if key not in {"body", "header", "footer"}:
|
||||
raise KeyError(f"Frame.contents has no key: {key!r}")
|
||||
try:
|
||||
value_w, value_options = value
|
||||
if value_options is not None:
|
||||
raise FrameError(f"added content invalid: {value!r}")
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise FrameError(f"added content invalid: {value!r}").with_traceback(exc.__traceback__) from exc
|
||||
if key == "body":
|
||||
self.body = value_w
|
||||
elif key == "footer":
|
||||
self.footer = value_w
|
||||
else:
|
||||
self.header = value_w
|
||||
|
||||
def _contents__delitem__(self, key: Literal["header", "footer"]) -> None:
|
||||
if key not in {"header", "footer"}:
|
||||
raise KeyError(f"Frame.contents can't remove key: {key!r}")
|
||||
if (key == "header" and self._header is None) or (key == "footer" and self._footer is None):
|
||||
raise KeyError(f"Frame.contents has no key: {key!r}")
|
||||
if key == "header":
|
||||
self.header = None
|
||||
else:
|
||||
self.footer = None
|
||||
|
||||
def _contents(self):
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}._contents` is deprecated, "
|
||||
f"please use property `{self.__class__.__name__}.contents`",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return self.contents
|
||||
|
||||
def options(self) -> None:
|
||||
"""
|
||||
There are currently no options for Frame contents.
|
||||
|
||||
Return None as a placeholder for future options.
|
||||
"""
|
||||
|
||||
def frame_top_bottom(self, size: tuple[int, int], focus: bool) -> tuple[tuple[int, int], tuple[int, int]]:
|
||||
"""
|
||||
Calculate the number of rows for the header and footer.
|
||||
|
||||
:param size: See :meth:`Widget.render` for details
|
||||
:type size: widget size
|
||||
:param focus: ``True`` if this widget is in focus
|
||||
:type focus: bool
|
||||
:returns: `(head rows, foot rows),(orig head, orig foot)`
|
||||
orig head/foot are from rows() calls.
|
||||
:rtype: (int, int), (int, int)
|
||||
"""
|
||||
(maxcol, maxrow) = size
|
||||
frows = hrows = 0
|
||||
|
||||
if self.header:
|
||||
hrows = self.header.rows((maxcol,), self.focus_part == "header" and focus)
|
||||
|
||||
if self.footer:
|
||||
frows = self.footer.rows((maxcol,), self.focus_part == "footer" and focus)
|
||||
|
||||
remaining = maxrow
|
||||
|
||||
if self.focus_part == "footer":
|
||||
if frows >= remaining:
|
||||
return (0, remaining), (hrows, frows)
|
||||
|
||||
remaining -= frows
|
||||
if hrows >= remaining:
|
||||
return (remaining, frows), (hrows, frows)
|
||||
|
||||
elif self.focus_part == "header":
|
||||
if hrows >= maxrow:
|
||||
return (remaining, 0), (hrows, frows)
|
||||
|
||||
remaining -= hrows
|
||||
if frows >= remaining:
|
||||
return (hrows, remaining), (hrows, frows)
|
||||
|
||||
elif hrows + frows >= remaining:
|
||||
# self.focus_part == 'body'
|
||||
rless1 = max(0, remaining - 1)
|
||||
if frows >= remaining - 1:
|
||||
return (0, rless1), (hrows, frows)
|
||||
|
||||
remaining -= frows
|
||||
rless1 = max(0, remaining - 1)
|
||||
return (rless1, frows), (hrows, frows)
|
||||
|
||||
return (hrows, frows), (hrows, frows)
|
||||
|
||||
def render(
|
||||
self,
|
||||
size: tuple[int, int], # type: ignore[override]
|
||||
focus: bool = False,
|
||||
) -> CompositeCanvas:
|
||||
(maxcol, maxrow) = size
|
||||
(htrim, ftrim), (hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus)
|
||||
|
||||
combinelist = []
|
||||
depends_on = []
|
||||
|
||||
head = None
|
||||
if htrim and htrim < hrows:
|
||||
head = Filler(self.header, VAlign.TOP).render((maxcol, htrim), focus and self.focus_part == "header")
|
||||
elif htrim:
|
||||
head = self.header.render((maxcol,), focus and self.focus_part == "header")
|
||||
if head.rows() != hrows:
|
||||
raise RuntimeError("rows, render mismatch")
|
||||
if head:
|
||||
combinelist.append((head, "header", self.focus_part == "header"))
|
||||
depends_on.append(self.header)
|
||||
|
||||
if ftrim + htrim < maxrow:
|
||||
body = self.body.render((maxcol, maxrow - ftrim - htrim), focus and self.focus_part == "body")
|
||||
combinelist.append((body, "body", self.focus_part == "body"))
|
||||
depends_on.append(self.body)
|
||||
|
||||
foot = None
|
||||
if ftrim and ftrim < frows:
|
||||
foot = Filler(self.footer, VAlign.BOTTOM).render((maxcol, ftrim), focus and self.focus_part == "footer")
|
||||
elif ftrim:
|
||||
foot = self.footer.render((maxcol,), focus and self.focus_part == "footer")
|
||||
if foot.rows() != frows:
|
||||
raise RuntimeError("rows, render mismatch")
|
||||
if foot:
|
||||
combinelist.append((foot, "footer", self.focus_part == "footer"))
|
||||
depends_on.append(self.footer)
|
||||
|
||||
return CanvasCombine(combinelist)
|
||||
|
||||
def keypress(
|
||||
self,
|
||||
size: tuple[int, int], # type: ignore[override]
|
||||
key: str,
|
||||
) -> str | None:
|
||||
"""Pass keypress to widget in focus."""
|
||||
(maxcol, maxrow) = size
|
||||
|
||||
if self.focus_part == "header" and self.header is not None:
|
||||
if not self.header.selectable():
|
||||
return key
|
||||
return self.header.keypress((maxcol,), key)
|
||||
if self.focus_part == "footer" and self.footer is not None:
|
||||
if not self.footer.selectable():
|
||||
return key
|
||||
return self.footer.keypress((maxcol,), key)
|
||||
if self.focus_part != "body":
|
||||
return key
|
||||
remaining = maxrow
|
||||
if self.header is not None:
|
||||
remaining -= self.header.rows((maxcol,))
|
||||
if self.footer is not None:
|
||||
remaining -= self.footer.rows((maxcol,))
|
||||
if remaining <= 0:
|
||||
return key
|
||||
|
||||
if not self.body.selectable():
|
||||
return key
|
||||
return self.body.keypress((maxcol, remaining), key)
|
||||
|
||||
def mouse_event(
|
||||
self,
|
||||
size: tuple[int, int], # type: ignore[override]
|
||||
event: str,
|
||||
button: int,
|
||||
col: int,
|
||||
row: int,
|
||||
focus: bool,
|
||||
) -> bool | None:
|
||||
"""
|
||||
Pass mouse event to appropriate part of frame.
|
||||
Focus may be changed on button 1 press.
|
||||
"""
|
||||
(maxcol, maxrow) = size
|
||||
(htrim, ftrim), (_hrows, _frows) = self.frame_top_bottom((maxcol, maxrow), focus)
|
||||
|
||||
if row < htrim: # within header
|
||||
focus = focus and self.focus_part == "header"
|
||||
if is_mouse_press(event) and button == 1 and self.header.selectable():
|
||||
self.focus_position = "header"
|
||||
if not hasattr(self.header, "mouse_event"):
|
||||
return False
|
||||
return self.header.mouse_event((maxcol,), event, button, col, row, focus)
|
||||
|
||||
if row >= maxrow - ftrim: # within footer
|
||||
focus = focus and self.focus_part == "footer"
|
||||
if is_mouse_press(event) and button == 1 and self.footer.selectable():
|
||||
self.focus_position = "footer"
|
||||
if not hasattr(self.footer, "mouse_event"):
|
||||
return False
|
||||
return self.footer.mouse_event((maxcol,), event, button, col, row - maxrow + ftrim, focus)
|
||||
|
||||
# within body
|
||||
focus = focus and self.focus_part == "body"
|
||||
if is_mouse_press(event) and button == 1 and self.body.selectable():
|
||||
self.focus_position = "body"
|
||||
|
||||
if not hasattr(self.body, "mouse_event"):
|
||||
return False
|
||||
return self.body.mouse_event((maxcol, maxrow - htrim - ftrim), event, button, col, row - htrim, focus)
|
||||
|
||||
def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
|
||||
"""Return the cursor coordinates of the focus widget."""
|
||||
if not self.focus.selectable():
|
||||
return None
|
||||
if not hasattr(self.focus, "get_cursor_coords"):
|
||||
return None
|
||||
|
||||
fp = self.focus_position
|
||||
(maxcol, maxrow) = size
|
||||
(hrows, frows), _ = self.frame_top_bottom(size, True)
|
||||
|
||||
if fp == "header":
|
||||
row_adjust = 0
|
||||
coords = self.header.get_cursor_coords((maxcol,))
|
||||
elif fp == "body":
|
||||
row_adjust = hrows
|
||||
coords = self.body.get_cursor_coords((maxcol, maxrow - hrows - frows))
|
||||
else:
|
||||
row_adjust = maxrow - frows
|
||||
coords = self.footer.get_cursor_coords((maxcol,))
|
||||
|
||||
if coords is None:
|
||||
return None
|
||||
|
||||
x, y = coords
|
||||
return x, y + row_adjust
|
||||
|
||||
def __iter__(self) -> Iterator[Literal["header", "body", "footer"]]:
|
||||
"""
|
||||
Return an iterator over the positions in this Frame top to bottom.
|
||||
"""
|
||||
if self._header:
|
||||
yield "header"
|
||||
yield "body"
|
||||
if self._footer:
|
||||
yield "footer"
|
||||
|
||||
def __reversed__(self) -> Iterator[Literal["footer", "body", "header"]]:
|
||||
"""
|
||||
Return an iterator over the positions in this Frame bottom to top.
|
||||
"""
|
||||
if self._footer:
|
||||
yield "footer"
|
||||
yield "body"
|
||||
if self._header:
|
||||
yield "header"
|
||||
Reference in New Issue
Block a user