Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
filler.py411 linesDownload Raw Back to widget
1from __future__ import annotations2 3import typing4import warnings5 6from urwid.canvas import CompositeCanvas7from urwid.split_repr import remove_defaults8from urwid.util import int_scale9 10from .constants import (11    RELATIVE_100,12    Sizing,13    VAlign,14    WHSettings,15    normalize_height,16    normalize_valign,17    simplify_height,18    simplify_valign,19)20from .widget_decoration import WidgetDecoration, WidgetError21 22if typing.TYPE_CHECKING:23    from typing_extensions import Literal24 25WrappedWidget = typing.TypeVar("WrappedWidget")26 27 28class FillerError(WidgetError):29    pass30 31 32class Filler(WidgetDecoration[WrappedWidget]):33    def __init__(34        self,35        body: WrappedWidget,36        valign: (37            Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]38        ) = VAlign.MIDDLE,39        height: (40            int | Literal["pack", WHSettings.PACK] | tuple[Literal["relative", WHSettings.RELATIVE], int] | None41        ) = WHSettings.PACK,42        min_height: int | None = None,43        top: int = 0,44        bottom: int = 0,45    ) -> None:46        """47        :param body: a flow widget or box widget to be filled around (stored as self.original_widget)48        :type body: Widget49 50        :param valign: one of:51            ``'top'``, ``'middle'``, ``'bottom'``,52            (``'relative'``, *percentage* 0=top 100=bottom)53 54        :param height: one of:55 56            ``'pack'``57              if body is a flow widget58 59            *given height*60              integer number of rows for self.original_widget61 62            (``'relative'``, *percentage of total height*)63              make height depend on container's height64 65        :param min_height: one of:66 67            ``None``68              if no minimum or if body is a flow widget69 70            *minimum height*71              integer number of rows for the widget when height not fixed72 73        :param top: a fixed number of rows to fill at the top74        :type top: int75        :param bottom: a fixed number of rows to fill at the bottom76        :type bottom: int77 78        If body is a flow widget, then height must be ``'pack'`` and *min_height* will be ignored.79        Sizing of the filler will be BOX/FLOW in this case.80 81        If height is integer, *min_height* will be ignored and sizing of filler will be BOX/FLOW.82 83        Filler widgets will try to satisfy height argument first by reducing the valign amount when necessary.84        If height still cannot be satisfied it will also be reduced.85        """86        super().__init__(body)87 88        # convert old parameters to the new top/bottom values89        if isinstance(height, tuple):90            if height[0] == "fixed top":91                if not isinstance(valign, tuple) or valign[0] != "fixed bottom":92                    raise FillerError("fixed top height may only be used with fixed bottom valign")93                top = height[1]94                height = RELATIVE_10095            elif height[0] == "fixed bottom":96                if not isinstance(valign, tuple) or valign[0] != "fixed top":97                    raise FillerError("fixed bottom height may only be used with fixed top valign")98                bottom = height[1]99                height = RELATIVE_100100 101        if isinstance(valign, tuple):102            if valign[0] == "fixed top":103                top = valign[1]104                normalized_valign = VAlign.TOP105            elif valign[0] == "fixed bottom":106                bottom = valign[1]107                normalized_valign = VAlign.BOTTOM108            else:109                normalized_valign = valign110 111        elif not isinstance(valign, (VAlign, str)):112            raise FillerError(f"invalid valign: {valign!r}")113 114        else:115            normalized_valign = VAlign(valign)116 117        # convert old flow mode parameter height=None to height='flow'118        if height is None or height == Sizing.FLOW:119            height = WHSettings.PACK120 121        self.top = top122        self.bottom = bottom123        self.valign_type, self.valign_amount = normalize_valign(normalized_valign, FillerError)124        self.height_type, self.height_amount = normalize_height(height, FillerError)125 126        if self.height_type not in {WHSettings.GIVEN, WHSettings.PACK}:127            self.min_height = min_height128        else:129            self.min_height = None130 131    def sizing(self) -> frozenset[Sizing]:132        """Widget sizing.133 134        Sizing BOX is always supported.135        Sizing FLOW is supported if: FLOW widget (a height type is PACK) or BOX widget with height GIVEN136        """137        sizing: set[Sizing] = {Sizing.BOX}138        if self.height_type in {WHSettings.PACK, WHSettings.GIVEN}:139            sizing.add(Sizing.FLOW)140        return frozenset(sizing)141 142    def rows(self, size: tuple[int], focus: bool = False) -> int:143        """Flow pack support if FLOW sizing supported."""144        if self.height_type == WHSettings.PACK:145            return self.original_widget.rows(size, focus) + self.top + self.bottom146        if self.height_type == WHSettings.GIVEN:147            return self.height_amount + self.top + self.bottom148        raise FillerError("Method 'rows' not supported for BOX widgets")  # pragma: no cover149 150    def _repr_attrs(self) -> dict[str, typing.Any]:151        attrs = {152            **super()._repr_attrs(),153            "valign": simplify_valign(self.valign_type, self.valign_amount),154            "height": simplify_height(self.height_type, self.height_amount),155            "top": self.top,156            "bottom": self.bottom,157            "min_height": self.min_height,158        }159        return remove_defaults(attrs, Filler.__init__)160 161    @property162    def body(self) -> WrappedWidget:163        """backwards compatibility, widget used to be stored as body"""164        warnings.warn(165            "backwards compatibility, widget used to be stored as body. API will be removed in version 5.0.",166            DeprecationWarning,167            stacklevel=2,168        )169        return self.original_widget170 171    @body.setter172    def body(self, new_body: WrappedWidget) -> None:173        warnings.warn(174            "backwards compatibility, widget used to be stored as body. API will be removed in version 5.0.",175            DeprecationWarning,176            stacklevel=2,177        )178        self.original_widget = new_body179 180    def get_body(self) -> WrappedWidget:181        """backwards compatibility, widget used to be stored as body"""182        warnings.warn(183            "backwards compatibility, widget used to be stored as body. API will be removed in version 4.0.",184            DeprecationWarning,185            stacklevel=2,186        )187        return self.original_widget188 189    def set_body(self, new_body: WrappedWidget) -> None:190        warnings.warn(191            "backwards compatibility, widget used to be stored as body. API will be removed in version 4.0.",192            DeprecationWarning,193            stacklevel=2,194        )195        self.original_widget = new_body196 197    def selectable(self) -> bool:198        """Return selectable from body."""199        return self._original_widget.selectable()200 201    def filler_values(self, size: tuple[int, int] | tuple[int], focus: bool) -> tuple[int, int]:202        """203        Return the number of rows to pad on the top and bottom.204 205        Override this method to define custom padding behaviour.206        """207        maxcol, maxrow = self.pack(size, focus)208 209        if self.height_type == WHSettings.PACK:210            height = self._original_widget.rows((maxcol,), focus=focus)211            return calculate_top_bottom_filler(212                maxrow,213                self.valign_type,214                self.valign_amount,215                WHSettings.GIVEN,216                height,217                None,218                self.top,219                self.bottom,220            )221 222        return calculate_top_bottom_filler(223            maxrow,224            self.valign_type,225            self.valign_amount,226            self.height_type,227            self.height_amount,228            self.min_height,229            self.top,230            self.bottom,231        )232 233    def render(234        self,235        size: tuple[int, int] | tuple[int],  # type: ignore[override]236        focus: bool = False,237    ) -> CompositeCanvas:238        """Render self.original_widget with space above and/or below."""239        maxcol, maxrow = self.pack(size, focus)240        top, bottom = self.filler_values(size, focus)241 242        if self.height_type == WHSettings.PACK:243            canv = self._original_widget.render((maxcol,), focus)244        else:245            canv = self._original_widget.render((maxcol, maxrow - top - bottom), focus)246        canv = CompositeCanvas(canv)247 248        if maxrow and canv.rows() > maxrow and canv.cursor is not None:249            _cx, cy = canv.cursor250            if cy >= maxrow:251                canv.trim(cy - maxrow + 1, maxrow - top - bottom)252        if canv.rows() > maxrow:253            canv.trim(0, maxrow)254            return canv255        canv.pad_trim_top_bottom(top, bottom)256        return canv257 258    def keypress(259        self,260        size: tuple[int, int] | tuple[()],  # type: ignore[override]261        key: str,262    ) -> str | None:263        """Pass keypress to self.original_widget."""264        maxcol, maxrow = self.pack(size, True)265        if self.height_type == WHSettings.PACK:266            return self._original_widget.keypress((maxcol,), key)267 268        top, bottom = self.filler_values((maxcol, maxrow), True)269        return self._original_widget.keypress((maxcol, maxrow - top - bottom), key)270 271    def get_cursor_coords(self, size: tuple[int, int] | tuple[int]) -> tuple[int, int] | None:272        """Return cursor coords from self.original_widget if any."""273        maxcol, maxrow = self.pack(size, True)274        if not hasattr(self._original_widget, "get_cursor_coords"):275            return None276 277        top, bottom = self.filler_values(size, True)278        if self.height_type == WHSettings.PACK:279            coords = self._original_widget.get_cursor_coords((maxcol,))280        else:281            coords = self._original_widget.get_cursor_coords((maxcol, maxrow - top - bottom))282        if not coords:283            return None284        x, y = coords285        if y >= maxrow:286            y = maxrow - 1287        return x, y + top288 289    def get_pref_col(self, size: tuple[int, int] | tuple[int]) -> int | None:290        """Return pref_col from self.original_widget if any."""291        maxcol, maxrow = self.pack(size, True)292        if not hasattr(self._original_widget, "get_pref_col"):293            return None294 295        if self.height_type == WHSettings.PACK:296            x = self._original_widget.get_pref_col((maxcol,))297        else:298            top, bottom = self.filler_values(size, True)299            x = self._original_widget.get_pref_col((maxcol, maxrow - top - bottom))300 301        return x302 303    def move_cursor_to_coords(self, size: tuple[int, int] | tuple[int], col: int, row: int) -> bool:304        """Pass to self.original_widget."""305        maxcol, maxrow = self.pack(size, True)306        if not hasattr(self._original_widget, "move_cursor_to_coords"):307            return True308 309        top, bottom = self.filler_values(size, True)310        if row < top or row >= maxcol - bottom:311            return False312 313        if self.height_type == WHSettings.PACK:314            return self._original_widget.move_cursor_to_coords((maxcol,), col, row - top)315        return self._original_widget.move_cursor_to_coords((maxcol, maxrow - top - bottom), col, row - top)316 317    def mouse_event(318        self,319        size: tuple[int, int] | tuple[int],  # type: ignore[override]320        event,321        button: int,322        col: int,323        row: int,324        focus: bool,325    ) -> bool | None:326        """Pass to self.original_widget."""327        maxcol, maxrow = self.pack(size, focus)328        if not hasattr(self._original_widget, "mouse_event"):329            return False330 331        top, bottom = self.filler_values(size, True)332        if row < top or row >= maxrow - bottom:333            return False334 335        if self.height_type == WHSettings.PACK:336            return self._original_widget.mouse_event((maxcol,), event, button, col, row - top, focus)337        return self._original_widget.mouse_event((maxcol, maxrow - top - bottom), event, button, col, row - top, focus)338 339 340def calculate_top_bottom_filler(341    maxrow: int,342    valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,343    valign_amount: int,344    height_type: Literal["given", "relative", "clip", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.CLIP],345    height_amount: int,346    min_height: int | None,347    top: int,348    bottom: int,349) -> tuple[int, int]:350    """351    Return the amount of filler (or clipping) on the top and352    bottom part of maxrow rows to satisfy the following:353 354    valign_type -- 'top', 'middle', 'bottom', 'relative'355    valign_amount -- a percentage when align_type=='relative'356    height_type -- 'given', 'relative', 'clip'357    height_amount -- a percentage when width_type=='relative'358        otherwise equal to the height of the widget359    min_height -- a desired minimum width for the widget or None360    top -- a fixed number of rows to fill on the top361    bottom -- a fixed number of rows to fill on the bottom362 363    >>> ctbf = calculate_top_bottom_filler364    >>> ctbf(15, "top", 0, "given", 10, None, 2, 0)365    (2, 3)366    >>> ctbf(15, "relative", 0, "given", 10, None, 2, 0)367    (2, 3)368    >>> ctbf(15, "relative", 100, "given", 10, None, 2, 0)369    (5, 0)370    >>> ctbf(15, "middle", 0, "given", 4, None, 2, 0)371    (6, 5)372    >>> ctbf(15, "middle", 0, "given", 18, None, 2, 0)373    (0, 0)374    >>> ctbf(20, "top", 0, "relative", 60, None, 0, 0)375    (0, 8)376    >>> ctbf(20, "relative", 30, "relative", 60, None, 0, 0)377    (2, 6)378    >>> ctbf(20, "relative", 30, "relative", 60, 14, 0, 0)379    (2, 4)380    """381    if height_type == WHSettings.RELATIVE:382        maxheight = max(maxrow - top - bottom, 0)383        height = int_scale(height_amount, 101, maxheight + 1)384        if min_height is not None:385            height = max(height, min_height)386    else:387        height = height_amount388 389    valign = {VAlign.TOP: 0, VAlign.MIDDLE: 50, VAlign.BOTTOM: 100}.get(valign_type, valign_amount)390 391    # add the remainder of top/bottom to the filler392    filler = maxrow - height - top - bottom393    bottom += int_scale(100 - valign, 101, filler + 1)394    top = maxrow - height - bottom395 396    # reduce filler if we are clipping an edge397    if bottom < 0 < top:398        shift = min(top, -bottom)399        top -= shift400        bottom += shift401    elif top < 0 < bottom:402        shift = min(bottom, -top)403        bottom -= shift404        top += shift405 406    # no negative values for filler at the moment407    top = max(top, 0)408    bottom = max(bottom, 0)409 410    return top, bottom411 
codekingpro/portable-devtools · Team Ai