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

370 lines
12 KiB
Python
Raw Normal View History

2024-05-25 16:45:07 +00:00
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
if typing.TYPE_CHECKING:
from typing_extensions import Literal
from .widget import Widget
class FillerError(Exception):
pass
class Filler(WidgetDecoration):
def __init__(
self,
body: Widget,
valign: (
Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]
) = VAlign.MIDDLE,
height: int | Literal["pack"] | tuple[Literal["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 ``'flow'`` and
*min_height* will be ignored.
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]
valign = VAlign.TOP
elif valign[0] == "fixed bottom":
bottom = valign[1]
valign = VAlign.BOTTOM
# 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(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):
return {Sizing.BOX} # always a box widget
def _repr_attrs(self):
attrs = dict(
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):
"""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):
warnings.warn(
"backwards compatibility, widget used to be stored as body",
PendingDeprecationWarning,
stacklevel=2,
)
self.original_widget = new_body
def get_body(self):
"""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):
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], 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) = size
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], focus: bool = False) -> CompositeCanvas:
"""Render self.original_widget with space above and/or below."""
(maxcol, maxrow) = size
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], key: str) -> str | None:
"""Pass keypress to self.original_widget."""
(maxcol, maxrow) = size
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, int] | None:
"""Return cursor coords from self.original_widget if any."""
(maxcol, maxrow) = size
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]) -> int:
"""Return pref_col from self.original_widget if any."""
(maxcol, maxrow) = size
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], col: int, row: int) -> bool:
"""Pass to self.original_widget."""
(maxcol, maxrow) = size
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],
event,
button: int,
col: int,
row: int,
focus: bool,
) -> bool:
"""Pass to self.original_widget."""
(maxcol, maxrow) = size
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
standard_alignments = {VAlign.TOP: 0, VAlign.MIDDLE: 50, VAlign.BOTTOM: 100}
valign = standard_alignments.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