Shofel2_T124_python/venv/lib/python3.10/site-packages/urwid/widget/frame.py

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"