698 lines
24 KiB
Python
698 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import abc
|
|
import typing
|
|
import warnings
|
|
|
|
from urwid.canvas import CanvasCombine, CompositeCanvas
|
|
from urwid.util import is_mouse_press
|
|
|
|
from .constants import Sizing
|
|
from .filler import Filler
|
|
from .widget import Widget
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
from typing_extensions import Literal
|
|
|
|
|
|
class WidgetContainerMixin:
|
|
"""
|
|
Mixin class for widget containers implementing common container methods
|
|
"""
|
|
|
|
def __getitem__(self, position) -> Widget:
|
|
"""
|
|
Container short-cut for self.contents[position][0].base_widget
|
|
which means "give me the child widget at position without any
|
|
widget decorations".
|
|
|
|
This allows for concise traversal of nested container widgets
|
|
such as:
|
|
|
|
my_widget[position0][position1][position2] ...
|
|
"""
|
|
return self.contents[position][0].base_widget
|
|
|
|
def get_focus_path(self):
|
|
"""
|
|
Return the .focus_position values starting from this container
|
|
and proceeding along each child widget until reaching a leaf
|
|
(non-container) widget.
|
|
"""
|
|
out = []
|
|
w = self
|
|
while True:
|
|
try:
|
|
p = w.focus_position
|
|
except IndexError:
|
|
return out
|
|
out.append(p)
|
|
w = w.focus.base_widget
|
|
|
|
def set_focus_path(self, positions):
|
|
"""
|
|
Set the .focus_position property starting from this container
|
|
widget and proceeding along newly focused child widgets. Any
|
|
failed assignment due do incompatible position types or invalid
|
|
positions will raise an IndexError.
|
|
|
|
This method may be used to restore a particular widget to the
|
|
focus by passing in the value returned from an earlier call to
|
|
get_focus_path().
|
|
|
|
positions -- sequence of positions
|
|
"""
|
|
w = self
|
|
for p in positions:
|
|
if p != w.focus_position:
|
|
w.focus_position = p # modifies w.focus
|
|
w = w.focus.base_widget
|
|
|
|
def get_focus_widgets(self) -> list[Widget]:
|
|
"""
|
|
Return the .focus values starting from this container
|
|
and proceeding along each child widget until reaching a leaf
|
|
(non-container) widget.
|
|
|
|
Note that the list does not contain the topmost container widget
|
|
(i.e., on which this method is called), but does include the
|
|
lowest leaf widget.
|
|
"""
|
|
out = []
|
|
w = self
|
|
while True:
|
|
w = w.base_widget.focus
|
|
if w is None:
|
|
return out
|
|
out.append(w)
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def focus(self) -> Widget:
|
|
"""
|
|
Read-only property returning the child widget in focus for
|
|
container widgets. This default implementation
|
|
always returns ``None``, indicating that this widget has no children.
|
|
"""
|
|
|
|
def _get_focus(self) -> Widget:
|
|
warnings.warn(
|
|
f"method `{self.__class__.__name__}._get_focus` is deprecated, "
|
|
f"please use `{self.__class__.__name__}.focus` property",
|
|
DeprecationWarning,
|
|
stacklevel=3,
|
|
)
|
|
return self.focus
|
|
|
|
|
|
class WidgetContainerListContentsMixin:
|
|
"""
|
|
Mixin class for widget containers whose positions are indexes into
|
|
a list available as self.contents.
|
|
"""
|
|
|
|
def __iter__(self) -> Iterator[int]:
|
|
"""
|
|
Return an iterable of positions for this container from first
|
|
to last.
|
|
"""
|
|
return iter(range(len(self.contents)))
|
|
|
|
def __reversed__(self) -> Iterator[int]:
|
|
"""
|
|
Return an iterable of positions for this container from last
|
|
to first.
|
|
"""
|
|
return iter(range(len(self.contents) - 1, -1, -1))
|
|
|
|
def __len__(self) -> int:
|
|
return len(self.contents)
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def contents(self) -> list[tuple[Widget, typing.Any]]:
|
|
"""The contents of container as a list of (widget, options)"""
|
|
|
|
@contents.setter
|
|
def contents(self, new_contents: list[tuple[Widget, typing.Any]]) -> None:
|
|
"""The contents of container as a list of (widget, options)"""
|
|
|
|
def _get_contents(self) -> list[tuple[Widget, typing.Any]]:
|
|
warnings.warn(
|
|
f"method `{self.__class__.__name__}._get_contents` is deprecated, "
|
|
f"please use `{self.__class__.__name__}.contents` property",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self.contents
|
|
|
|
def _set_contents(self, c: list[tuple[Widget, typing.Any]]) -> None:
|
|
warnings.warn(
|
|
f"method `{self.__class__.__name__}._set_contents` is deprecated, "
|
|
f"please use `{self.__class__.__name__}.contents` property",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
self.contents = c
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def focus_position(self) -> int | None:
|
|
"""
|
|
index of child widget in focus.
|
|
"""
|
|
|
|
@focus_position.setter
|
|
def focus_position(self, position: int) -> None:
|
|
"""
|
|
index of child widget in focus.
|
|
"""
|
|
|
|
def _get_focus_position(self) -> int | None:
|
|
warnings.warn(
|
|
f"method `{self.__class__.__name__}._get_focus_position` is deprecated, "
|
|
f"please use `{self.__class__.__name__}.focus_position` property",
|
|
DeprecationWarning,
|
|
stacklevel=3,
|
|
)
|
|
return self.focus_position
|
|
|
|
def _set_focus_position(self, position: int) -> None:
|
|
"""
|
|
Set the widget in focus.
|
|
|
|
position -- index of child widget to be made focus
|
|
"""
|
|
warnings.warn(
|
|
f"method `{self.__class__.__name__}._set_focus_position` is deprecated, "
|
|
f"please use `{self.__class__.__name__}.focus_position` property",
|
|
DeprecationWarning,
|
|
stacklevel=3,
|
|
)
|
|
self.focus_position = position
|
|
|
|
|
|
class FrameError(Exception):
|
|
pass
|
|
|
|
|
|
class Frame(Widget, WidgetContainerMixin):
|
|
"""
|
|
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: Widget,
|
|
header: Widget | None = None,
|
|
footer: Widget | None = None,
|
|
focus_part: Literal["header", "footer", "body"] = "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
|
|
"""
|
|
super().__init__()
|
|
|
|
self._header = header
|
|
self._body = body
|
|
self._footer = footer
|
|
self.focus_part = focus_part
|
|
|
|
@property
|
|
def header(self) -> Widget | None:
|
|
return self._header
|
|
|
|
@header.setter
|
|
def header(self, header: Widget | None):
|
|
self._header = header
|
|
if header is None and self.focus_part == "header":
|
|
self.focus_part = "body"
|
|
self._invalidate()
|
|
|
|
def get_header(self) -> Widget | None:
|
|
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: Widget | 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) -> Widget:
|
|
return self._body
|
|
|
|
@body.setter
|
|
def body(self, body: Widget) -> None:
|
|
self._body = body
|
|
self._invalidate()
|
|
|
|
def get_body(self) -> Widget:
|
|
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: Widget) -> 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) -> Widget | None:
|
|
return self._footer
|
|
|
|
@footer.setter
|
|
def footer(self, footer: Widget | None) -> None:
|
|
self._footer = footer
|
|
if footer is None and self.focus_part == "footer":
|
|
self.focus_part = "body"
|
|
self._invalidate()
|
|
|
|
def get_footer(self) -> Widget | None:
|
|
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: Widget | None) -> 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) -> Widget:
|
|
"""
|
|
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) -> Widget:
|
|
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):
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
class FrameContents:
|
|
def __len__(inner_self):
|
|
return len(inner_self.keys())
|
|
|
|
def items(inner_self):
|
|
return [(k, inner_self[k]) for k in inner_self]
|
|
|
|
def values(inner_self):
|
|
return [inner_self[k] for k in inner_self]
|
|
|
|
def update(inner_self, E=None, **F):
|
|
if E:
|
|
keys = getattr(E, "keys", None)
|
|
if keys:
|
|
for k in E:
|
|
inner_self[k] = E[k]
|
|
else:
|
|
for k, v in E:
|
|
inner_self[k] = v
|
|
for k in F:
|
|
inner_self[k] = F[k]
|
|
|
|
keys = self._contents_keys
|
|
__getitem__ = self._contents__getitem__
|
|
__setitem__ = self._contents__setitem__
|
|
__delitem__ = self._contents__delitem__
|
|
|
|
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
|
|
|
|
def _contents__getitem__(self, key: Literal["header", "footer", "body"]):
|
|
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}")
|
|
|
|
def _contents__setitem__(self, key: Literal["header", "footer", "body"], value):
|
|
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", "body"]):
|
|
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.
|
|
"""
|
|
return
|
|
|
|
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], 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, "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, "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], 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], event, 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):
|
|
"""
|
|
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):
|
|
"""
|
|
Return an iterator over the positions in this Frame bottom to top.
|
|
"""
|
|
if self._footer:
|
|
yield "footer"
|
|
yield "body"
|
|
if self._header:
|
|
yield "header"
|