Automated update
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from urwid.canvas import CompositeCanvas
|
||||
from urwid.split_repr import remove_defaults
|
||||
from urwid.util import int_scale
|
||||
|
||||
from .constants import (
|
||||
RELATIVE_100,
|
||||
Sizing,
|
||||
VAlign,
|
||||
WHSettings,
|
||||
normalize_height,
|
||||
normalize_valign,
|
||||
simplify_height,
|
||||
simplify_valign,
|
||||
)
|
||||
from .widget_decoration import WidgetDecoration, WidgetError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
WrappedWidget = typing.TypeVar("WrappedWidget")
|
||||
|
||||
|
||||
class FillerError(WidgetError):
|
||||
pass
|
||||
|
||||
|
||||
class Filler(WidgetDecoration[WrappedWidget]):
|
||||
def __init__(
|
||||
self,
|
||||
body: WrappedWidget,
|
||||
valign: (
|
||||
Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]
|
||||
) = VAlign.MIDDLE,
|
||||
height: (
|
||||
int | Literal["pack", WHSettings.PACK] | tuple[Literal["relative", WHSettings.RELATIVE], int] | None
|
||||
) = WHSettings.PACK,
|
||||
min_height: int | None = None,
|
||||
top: int = 0,
|
||||
bottom: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
:param body: a flow widget or box widget to be filled around (stored as self.original_widget)
|
||||
:type body: Widget
|
||||
|
||||
:param valign: one of:
|
||||
``'top'``, ``'middle'``, ``'bottom'``,
|
||||
(``'relative'``, *percentage* 0=top 100=bottom)
|
||||
|
||||
:param height: one of:
|
||||
|
||||
``'pack'``
|
||||
if body is a flow widget
|
||||
|
||||
*given height*
|
||||
integer number of rows for self.original_widget
|
||||
|
||||
(``'relative'``, *percentage of total height*)
|
||||
make height depend on container's height
|
||||
|
||||
:param min_height: one of:
|
||||
|
||||
``None``
|
||||
if no minimum or if body is a flow widget
|
||||
|
||||
*minimum height*
|
||||
integer number of rows for the widget when height not fixed
|
||||
|
||||
:param top: a fixed number of rows to fill at the top
|
||||
:type top: int
|
||||
:param bottom: a fixed number of rows to fill at the bottom
|
||||
:type bottom: int
|
||||
|
||||
If body is a flow widget, then height must be ``'pack'`` and *min_height* will be ignored.
|
||||
Sizing of the filler will be BOX/FLOW in this case.
|
||||
|
||||
If height is integer, *min_height* will be ignored and sizing of filler will be BOX/FLOW.
|
||||
|
||||
Filler widgets will try to satisfy height argument first by reducing the valign amount when necessary.
|
||||
If height still cannot be satisfied it will also be reduced.
|
||||
"""
|
||||
super().__init__(body)
|
||||
|
||||
# convert old parameters to the new top/bottom values
|
||||
if isinstance(height, tuple):
|
||||
if height[0] == "fixed top":
|
||||
if not isinstance(valign, tuple) or valign[0] != "fixed bottom":
|
||||
raise FillerError("fixed top height may only be used with fixed bottom valign")
|
||||
top = height[1]
|
||||
height = RELATIVE_100
|
||||
elif height[0] == "fixed bottom":
|
||||
if not isinstance(valign, tuple) or valign[0] != "fixed top":
|
||||
raise FillerError("fixed bottom height may only be used with fixed top valign")
|
||||
bottom = height[1]
|
||||
height = RELATIVE_100
|
||||
|
||||
if isinstance(valign, tuple):
|
||||
if valign[0] == "fixed top":
|
||||
top = valign[1]
|
||||
normalized_valign = VAlign.TOP
|
||||
elif valign[0] == "fixed bottom":
|
||||
bottom = valign[1]
|
||||
normalized_valign = VAlign.BOTTOM
|
||||
else:
|
||||
normalized_valign = valign
|
||||
|
||||
elif not isinstance(valign, (VAlign, str)):
|
||||
raise FillerError(f"invalid valign: {valign!r}")
|
||||
|
||||
else:
|
||||
normalized_valign = VAlign(valign)
|
||||
|
||||
# convert old flow mode parameter height=None to height='flow'
|
||||
if height is None or height == Sizing.FLOW:
|
||||
height = WHSettings.PACK
|
||||
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
self.valign_type, self.valign_amount = normalize_valign(normalized_valign, FillerError)
|
||||
self.height_type, self.height_amount = normalize_height(height, FillerError)
|
||||
|
||||
if self.height_type not in {WHSettings.GIVEN, WHSettings.PACK}:
|
||||
self.min_height = min_height
|
||||
else:
|
||||
self.min_height = None
|
||||
|
||||
def sizing(self) -> frozenset[Sizing]:
|
||||
"""Widget sizing.
|
||||
|
||||
Sizing BOX is always supported.
|
||||
Sizing FLOW is supported if: FLOW widget (a height type is PACK) or BOX widget with height GIVEN
|
||||
"""
|
||||
sizing: set[Sizing] = {Sizing.BOX}
|
||||
if self.height_type in {WHSettings.PACK, WHSettings.GIVEN}:
|
||||
sizing.add(Sizing.FLOW)
|
||||
return frozenset(sizing)
|
||||
|
||||
def rows(self, size: tuple[int], focus: bool = False) -> int:
|
||||
"""Flow pack support if FLOW sizing supported."""
|
||||
if self.height_type == WHSettings.PACK:
|
||||
return self.original_widget.rows(size, focus) + self.top + self.bottom
|
||||
if self.height_type == WHSettings.GIVEN:
|
||||
return self.height_amount + self.top + self.bottom
|
||||
raise FillerError("Method 'rows' not supported for BOX widgets") # pragma: no cover
|
||||
|
||||
def _repr_attrs(self) -> dict[str, typing.Any]:
|
||||
attrs = {
|
||||
**super()._repr_attrs(),
|
||||
"valign": simplify_valign(self.valign_type, self.valign_amount),
|
||||
"height": simplify_height(self.height_type, self.height_amount),
|
||||
"top": self.top,
|
||||
"bottom": self.bottom,
|
||||
"min_height": self.min_height,
|
||||
}
|
||||
return remove_defaults(attrs, Filler.__init__)
|
||||
|
||||
@property
|
||||
def body(self) -> WrappedWidget:
|
||||
"""backwards compatibility, widget used to be stored as body"""
|
||||
warnings.warn(
|
||||
"backwards compatibility, widget used to be stored as body",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.original_widget
|
||||
|
||||
@body.setter
|
||||
def body(self, new_body: WrappedWidget) -> None:
|
||||
warnings.warn(
|
||||
"backwards compatibility, widget used to be stored as body",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.original_widget = new_body
|
||||
|
||||
def get_body(self) -> WrappedWidget:
|
||||
"""backwards compatibility, widget used to be stored as body"""
|
||||
warnings.warn(
|
||||
"backwards compatibility, widget used to be stored as body",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.original_widget
|
||||
|
||||
def set_body(self, new_body: WrappedWidget) -> None:
|
||||
warnings.warn(
|
||||
"backwards compatibility, widget used to be stored as body",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.original_widget = new_body
|
||||
|
||||
def selectable(self) -> bool:
|
||||
"""Return selectable from body."""
|
||||
return self._original_widget.selectable()
|
||||
|
||||
def filler_values(self, size: tuple[int, int] | tuple[int], focus: bool) -> tuple[int, int]:
|
||||
"""
|
||||
Return the number of rows to pad on the top and bottom.
|
||||
|
||||
Override this method to define custom padding behaviour.
|
||||
"""
|
||||
maxcol, maxrow = self.pack(size, focus)
|
||||
|
||||
if self.height_type == WHSettings.PACK:
|
||||
height = self._original_widget.rows((maxcol,), focus=focus)
|
||||
return calculate_top_bottom_filler(
|
||||
maxrow,
|
||||
self.valign_type,
|
||||
self.valign_amount,
|
||||
WHSettings.GIVEN,
|
||||
height,
|
||||
None,
|
||||
self.top,
|
||||
self.bottom,
|
||||
)
|
||||
|
||||
return calculate_top_bottom_filler(
|
||||
maxrow,
|
||||
self.valign_type,
|
||||
self.valign_amount,
|
||||
self.height_type,
|
||||
self.height_amount,
|
||||
self.min_height,
|
||||
self.top,
|
||||
self.bottom,
|
||||
)
|
||||
|
||||
def render(
|
||||
self,
|
||||
size: tuple[int, int] | tuple[int], # type: ignore[override]
|
||||
focus: bool = False,
|
||||
) -> CompositeCanvas:
|
||||
"""Render self.original_widget with space above and/or below."""
|
||||
maxcol, maxrow = self.pack(size, focus)
|
||||
top, bottom = self.filler_values(size, focus)
|
||||
|
||||
if self.height_type == WHSettings.PACK:
|
||||
canv = self._original_widget.render((maxcol,), focus)
|
||||
else:
|
||||
canv = self._original_widget.render((maxcol, maxrow - top - bottom), focus)
|
||||
canv = CompositeCanvas(canv)
|
||||
|
||||
if maxrow and canv.rows() > maxrow and canv.cursor is not None:
|
||||
_cx, cy = canv.cursor
|
||||
if cy >= maxrow:
|
||||
canv.trim(cy - maxrow + 1, maxrow - top - bottom)
|
||||
if canv.rows() > maxrow:
|
||||
canv.trim(0, maxrow)
|
||||
return canv
|
||||
canv.pad_trim_top_bottom(top, bottom)
|
||||
return canv
|
||||
|
||||
def keypress(
|
||||
self,
|
||||
size: tuple[int, int] | tuple[()], # type: ignore[override]
|
||||
key: str,
|
||||
) -> str | None:
|
||||
"""Pass keypress to self.original_widget."""
|
||||
maxcol, maxrow = self.pack(size, True)
|
||||
if self.height_type == WHSettings.PACK:
|
||||
return self._original_widget.keypress((maxcol,), key)
|
||||
|
||||
top, bottom = self.filler_values((maxcol, maxrow), True)
|
||||
return self._original_widget.keypress((maxcol, maxrow - top - bottom), key)
|
||||
|
||||
def get_cursor_coords(self, size: tuple[int, int] | tuple[int]) -> tuple[int, int] | None:
|
||||
"""Return cursor coords from self.original_widget if any."""
|
||||
maxcol, maxrow = self.pack(size, True)
|
||||
if not hasattr(self._original_widget, "get_cursor_coords"):
|
||||
return None
|
||||
|
||||
top, bottom = self.filler_values(size, True)
|
||||
if self.height_type == WHSettings.PACK:
|
||||
coords = self._original_widget.get_cursor_coords((maxcol,))
|
||||
else:
|
||||
coords = self._original_widget.get_cursor_coords((maxcol, maxrow - top - bottom))
|
||||
if not coords:
|
||||
return None
|
||||
x, y = coords
|
||||
if y >= maxrow:
|
||||
y = maxrow - 1
|
||||
return x, y + top
|
||||
|
||||
def get_pref_col(self, size: tuple[int, int] | tuple[int]) -> int | None:
|
||||
"""Return pref_col from self.original_widget if any."""
|
||||
maxcol, maxrow = self.pack(size, True)
|
||||
if not hasattr(self._original_widget, "get_pref_col"):
|
||||
return None
|
||||
|
||||
if self.height_type == WHSettings.PACK:
|
||||
x = self._original_widget.get_pref_col((maxcol,))
|
||||
else:
|
||||
top, bottom = self.filler_values(size, True)
|
||||
x = self._original_widget.get_pref_col((maxcol, maxrow - top - bottom))
|
||||
|
||||
return x
|
||||
|
||||
def move_cursor_to_coords(self, size: tuple[int, int] | tuple[int], col: int, row: int) -> bool:
|
||||
"""Pass to self.original_widget."""
|
||||
maxcol, maxrow = self.pack(size, True)
|
||||
if not hasattr(self._original_widget, "move_cursor_to_coords"):
|
||||
return True
|
||||
|
||||
top, bottom = self.filler_values(size, True)
|
||||
if row < top or row >= maxcol - bottom:
|
||||
return False
|
||||
|
||||
if self.height_type == WHSettings.PACK:
|
||||
return self._original_widget.move_cursor_to_coords((maxcol,), col, row - top)
|
||||
return self._original_widget.move_cursor_to_coords((maxcol, maxrow - top - bottom), col, row - top)
|
||||
|
||||
def mouse_event(
|
||||
self,
|
||||
size: tuple[int, int] | tuple[int], # type: ignore[override]
|
||||
event,
|
||||
button: int,
|
||||
col: int,
|
||||
row: int,
|
||||
focus: bool,
|
||||
) -> bool | None:
|
||||
"""Pass to self.original_widget."""
|
||||
maxcol, maxrow = self.pack(size, focus)
|
||||
if not hasattr(self._original_widget, "mouse_event"):
|
||||
return False
|
||||
|
||||
top, bottom = self.filler_values(size, True)
|
||||
if row < top or row >= maxrow - bottom:
|
||||
return False
|
||||
|
||||
if self.height_type == WHSettings.PACK:
|
||||
return self._original_widget.mouse_event((maxcol,), event, button, col, row - top, focus)
|
||||
return self._original_widget.mouse_event((maxcol, maxrow - top - bottom), event, button, col, row - top, focus)
|
||||
|
||||
|
||||
def calculate_top_bottom_filler(
|
||||
maxrow: int,
|
||||
valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
|
||||
valign_amount: int,
|
||||
height_type: Literal["given", "relative", "clip", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.CLIP],
|
||||
height_amount: int,
|
||||
min_height: int | None,
|
||||
top: int,
|
||||
bottom: int,
|
||||
) -> tuple[int, int]:
|
||||
"""
|
||||
Return the amount of filler (or clipping) on the top and
|
||||
bottom part of maxrow rows to satisfy the following:
|
||||
|
||||
valign_type -- 'top', 'middle', 'bottom', 'relative'
|
||||
valign_amount -- a percentage when align_type=='relative'
|
||||
height_type -- 'given', 'relative', 'clip'
|
||||
height_amount -- a percentage when width_type=='relative'
|
||||
otherwise equal to the height of the widget
|
||||
min_height -- a desired minimum width for the widget or None
|
||||
top -- a fixed number of rows to fill on the top
|
||||
bottom -- a fixed number of rows to fill on the bottom
|
||||
|
||||
>>> ctbf = calculate_top_bottom_filler
|
||||
>>> ctbf(15, 'top', 0, 'given', 10, None, 2, 0)
|
||||
(2, 3)
|
||||
>>> ctbf(15, 'relative', 0, 'given', 10, None, 2, 0)
|
||||
(2, 3)
|
||||
>>> ctbf(15, 'relative', 100, 'given', 10, None, 2, 0)
|
||||
(5, 0)
|
||||
>>> ctbf(15, 'middle', 0, 'given', 4, None, 2, 0)
|
||||
(6, 5)
|
||||
>>> ctbf(15, 'middle', 0, 'given', 18, None, 2, 0)
|
||||
(0, 0)
|
||||
>>> ctbf(20, 'top', 0, 'relative', 60, None, 0, 0)
|
||||
(0, 8)
|
||||
>>> ctbf(20, 'relative', 30, 'relative', 60, None, 0, 0)
|
||||
(2, 6)
|
||||
>>> ctbf(20, 'relative', 30, 'relative', 60, 14, 0, 0)
|
||||
(2, 4)
|
||||
"""
|
||||
if height_type == WHSettings.RELATIVE:
|
||||
maxheight = max(maxrow - top - bottom, 0)
|
||||
height = int_scale(height_amount, 101, maxheight + 1)
|
||||
if min_height is not None:
|
||||
height = max(height, min_height)
|
||||
else:
|
||||
height = height_amount
|
||||
|
||||
valign = {VAlign.TOP: 0, VAlign.MIDDLE: 50, VAlign.BOTTOM: 100}.get(valign_type, valign_amount)
|
||||
|
||||
# add the remainder of top/bottom to the filler
|
||||
filler = maxrow - height - top - bottom
|
||||
bottom += int_scale(100 - valign, 101, filler + 1)
|
||||
top = maxrow - height - bottom
|
||||
|
||||
# reduce filler if we are clipping an edge
|
||||
if bottom < 0 < top:
|
||||
shift = min(top, -bottom)
|
||||
top -= shift
|
||||
bottom += shift
|
||||
elif top < 0 < bottom:
|
||||
shift = min(bottom, -top)
|
||||
bottom -= shift
|
||||
top += shift
|
||||
|
||||
# no negative values for filler at the moment
|
||||
top = max(top, 0)
|
||||
bottom = max(bottom, 0)
|
||||
|
||||
return top, bottom
|
||||
Reference in New Issue
Block a user