Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
grid_flow.py518 linesDownload Raw Back to widget
1from __future__ import annotations2 3import typing4import warnings5 6from urwid.split_repr import remove_defaults7 8from .columns import Columns9from .constants import Align, Sizing, WHSettings10from .container import WidgetContainerListContentsMixin, WidgetContainerMixin11from .divider import Divider12from .monitored_list import MonitoredFocusList, MonitoredList13from .padding import Padding14from .pile import Pile15from .widget import Widget, WidgetError, WidgetWarning, WidgetWrap16 17if typing.TYPE_CHECKING:18    from collections.abc import Iterable, Iterator, Sequence19 20    from typing_extensions import Literal21 22 23class GridFlowError(WidgetError):24    """GridFlow specific error."""25 26 27class GridFlowWarning(WidgetWarning):28    """GridFlow specific warning."""29 30 31class GridFlow(WidgetWrap[Pile], WidgetContainerMixin, WidgetContainerListContentsMixin):32    """33    The GridFlow widget is a flow widget that renders all the widgets it contains the same width,34    and it arranges them from left to right and top to bottom.35    """36 37    def sizing(self) -> frozenset[Sizing]:38        """Widget sizing.39 40        ..note:: Empty widget sizing is limited to the FLOW due to no data for width.41        """42        if self:43            return frozenset((Sizing.FLOW, Sizing.FIXED))44        return frozenset((Sizing.FLOW,))45 46    def __init__(47        self,48        cells: Iterable[Widget],49        cell_width: int,50        h_sep: int,51        v_sep: int,52        align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int],53        focus: int | Widget | None = None,54    ) -> None:55        """56        :param cells: iterable of flow widgets to display57        :param cell_width: column width for each cell58        :param h_sep: blank columns between each cell horizontally59        :param v_sep: blank rows between cells vertically60            (if more than one row is required to display all the cells)61        :param align: horizontal alignment of cells, one of:62            'left', 'center', 'right', ('relative', percentage 0=left 100=right)63        :param focus: widget index or widget instance to focus on64        """65        prepared_contents: list[tuple[Widget, tuple[Literal[WHSettings.GIVEN], int]]] = []66        focus_position: int = -167 68        for idx, widget in enumerate(cells):69            prepared_contents.append((widget, (WHSettings.GIVEN, cell_width)))70            if focus_position < 0 and (focus in {widget, idx} or (focus is None and widget.selectable())):71                focus_position = idx72 73        focus_position = max(focus_position, 0)74 75        self._contents: MonitoredFocusList[tuple[Widget, tuple[Literal[WHSettings.GIVEN], int]]] = MonitoredFocusList(76            prepared_contents, focus=focus_position77        )78        self._contents.set_modified_callback(self._invalidate)79        self._contents.set_focus_changed_callback(lambda f: self._invalidate())80        self._contents.set_validate_contents_modified(self._contents_modified)81        self._cell_width = cell_width82        self.h_sep = h_sep83        self.v_sep = v_sep84        self.align = align85        self._cache_maxcol = self._get_maxcol(())86        super().__init__(self.generate_display_widget((self._cache_maxcol,)))87 88    def _repr_words(self) -> list[str]:89        if len(self.contents) > 1:90            contents_string = f"({len(self.contents)} items)"91        elif self.contents:92            contents_string = "(1 item)"93        else:94            contents_string = "()"95        return [*super()._repr_words(), contents_string]96 97    def _repr_attrs(self) -> dict[str, typing.Any]:98        attrs = {99            **super()._repr_attrs(),100            "cell_width": self.cell_width,101            "h_sep": self.h_sep,102            "v_sep": self.v_sep,103            "align": self.align,104            "focus": self.focus_position if len(self._contents) > 1 else None,105        }106        return remove_defaults(attrs, GridFlow.__init__)107 108    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:109        yield "cells", [widget for widget, _ in self.contents]110        yield "cell_width", self.cell_width111        yield "h_sep", self.h_sep112        yield "v_sep", self.v_sep113        yield "align", self.align114        yield "focus", self.focus_position115 116    def __len__(self) -> int:117        return len(self._contents)118 119    def _invalidate(self) -> None:120        self._cache_maxcol = None121        super()._invalidate()122 123    def _contents_modified(124        self,125        _slc: tuple[int, int, int],126        new_items: Iterable[tuple[Widget, tuple[Literal["given", WHSettings.GIVEN], int]]],127    ) -> None:128        for item in new_items:129            try:130                _w, (t, _n) = item131                if t != WHSettings.GIVEN:132                    raise GridFlowError(f"added content invalid {item!r}")133            except (TypeError, ValueError) as exc:  # noqa: PERF203134                raise GridFlowError(f"added content invalid {item!r}").with_traceback(exc.__traceback__) from exc135 136    @property137    def cells(self):138        """139        A list of the widgets in this GridFlow140 141        .. note:: only for backwards compatibility. You should use the new142            standard container property :attr:`contents` to modify GridFlow143            contents.144        """145        warnings.warn(146            "only for backwards compatibility."147            "You should use the new standard container property `contents` to modify GridFlow."148            "API will be removed in version 5.0.",149            DeprecationWarning,150            stacklevel=2,151        )152        ml = MonitoredList(w for w, t in self.contents)153 154        def user_modified():155            self.cells = ml156 157        ml.set_modified_callback(user_modified)158        return ml159 160    @cells.setter161    def cells(self, widgets: Sequence[Widget]):162        warnings.warn(163            "only for backwards compatibility."164            "You should use the new standard container property `contents` to modify GridFlow."165            "API will be removed in version 5.0.",166            DeprecationWarning,167            stacklevel=2,168        )169        focus_position = self.focus_position170        self.contents = [(new, (WHSettings.GIVEN, self._cell_width)) for new in widgets]171        if focus_position < len(widgets):172            self.focus_position = focus_position173 174    @property175    def cell_width(self) -> int:176        """177        The width of each cell in the GridFlow. Setting this value affects178        all cells.179        """180        return self._cell_width181 182    @cell_width.setter183    def cell_width(self, width: int) -> None:184        focus_position = self.focus_position185        self.contents = [(w, (WHSettings.GIVEN, width)) for (w, options) in self.contents]186        self.focus_position = focus_position187        self._cell_width = width188 189    @property190    def contents(self) -> MonitoredFocusList[tuple[Widget, tuple[Literal[WHSettings.GIVEN], int]]]:191        """192        The contents of this GridFlow as a list of (widget, options)193        tuples.194 195        options is currently a tuple in the form `('fixed', number)`.196        number is the number of screen columns to allocate to this cell.197        'fixed' is the only type accepted at this time.198 199        This list may be modified like a normal list and the GridFlow200        widget will update automatically.201 202        .. seealso:: Create new options tuples with the :meth:`options` method.203        """204        return self._contents205 206    @contents.setter207    def contents(self, c):208        self._contents[:] = c209 210    def options(211        self,212        width_type: Literal["given", WHSettings.GIVEN] = WHSettings.GIVEN,213        width_amount: int | None = None,214    ) -> tuple[Literal[WHSettings.GIVEN], int]:215        """216        Return a new options tuple for use in a GridFlow's .contents list.217 218        width_type -- 'given' is the only value accepted219        width_amount -- None to use the default cell_width for this GridFlow220        """221        if width_type != WHSettings.GIVEN:222            raise GridFlowError(f"invalid width_type: {width_type!r}")223        if width_amount is None:224            width_amount = self._cell_width225        return (WHSettings(width_type), width_amount)226 227    def set_focus(self, cell: Widget | int) -> None:228        """229        Set the cell in focus, for backwards compatibility.230 231        .. note:: only for backwards compatibility. You may also use the new232            standard container property :attr:`focus_position` to get the focus.233 234        :param cell: contained element to focus235        :type cell: Widget or int236        """237        warnings.warn(238            "only for backwards compatibility."239            "You may also use the new standard container property `focus_position` to set the focus."240            "API will be removed in version 5.0.",241            DeprecationWarning,242            stacklevel=2,243        )244        if isinstance(cell, int):245            try:246                if cell < 0 or cell >= len(self.contents):247                    raise IndexError(f"No GridFlow child widget at position {cell}")248            except TypeError as exc:249                raise IndexError(f"No GridFlow child widget at position {cell}").with_traceback(250                    exc.__traceback__251                ) from exc252            self.contents.focus = cell253            return254 255        for i, (w, _options) in enumerate(self.contents):256            if cell == w:257                self.focus_position = i258                return259        raise ValueError(f"Widget not found in GridFlow contents: {cell!r}")260 261    @property262    def focus(self) -> Widget | None:263        """the child widget in focus or None when GridFlow is empty"""264        if not self.contents:265            return None266        return self.contents[self.focus_position][0]267 268    def get_focus(self):269        """270        Return the widget in focus, for backwards compatibility.271 272        .. note:: only for backwards compatibility. You may also use the new273            standard container property :attr:`focus` to get the focus.274        """275        warnings.warn(276            "only for backwards compatibility."277            "You may also use the new standard container property `focus` to get the focus."278            "API will be removed in version 5.0.",279            DeprecationWarning,280            stacklevel=2,281        )282        if not self.contents:283            return None284        return self.contents[self.focus_position][0]285 286    @property287    def focus_cell(self):288        warnings.warn(289            "only for backwards compatibility."290            "You may also use the new standard container property"291            "`focus` to get the focus and `focus_position` to get/set the cell in focus by index."292            "API will be removed in version 5.0.",293            DeprecationWarning,294            stacklevel=2,295        )296        return self.focus297 298    @focus_cell.setter299    def focus_cell(self, cell: Widget) -> None:300        warnings.warn(301            "only for backwards compatibility."302            "You may also use the new standard container property"303            "`focus` to get the focus and `focus_position` to get/set the cell in focus by index."304            "API will be removed in version 5.0.",305            DeprecationWarning,306            stacklevel=2,307        )308        for i, (w, _options) in enumerate(self.contents):309            if cell == w:310                self.focus_position = i311                return312        raise ValueError(f"Widget not found in GridFlow contents: {cell!r}")313 314    @property315    def focus_position(self) -> int | None:316        """317        index of child widget in focus.318        Raises :exc:`IndexError` if read when GridFlow is empty, or when set to an invalid index.319        """320        if not self.contents:321            raise IndexError("No focus_position, GridFlow is empty")322        return self.contents.focus323 324    @focus_position.setter325    def focus_position(self, position: int) -> None:326        """327        Set the widget in focus.328 329        position -- index of child widget to be made focus330        """331        try:332            if position < 0 or position >= len(self.contents):333                raise IndexError(f"No GridFlow child widget at position {position}")334        except TypeError as exc:335            raise IndexError(f"No GridFlow child widget at position {position}").with_traceback(336                exc.__traceback__337            ) from exc338        self.contents.focus = position339 340    def _get_maxcol(self, size: tuple[int] | tuple[()]) -> int:341        if size:342            (maxcol,) = size343            if self and maxcol < self.cell_width:344                warnings.warn(345                    f"Size is smaller than cell width ({maxcol!r} < {self.cell_width!r})",346                    GridFlowWarning,347                    stacklevel=3,348                )349        elif self:350            maxcol = len(self) * self.cell_width + (len(self) - 1) * self.h_sep351        else:352            maxcol = 0353        return maxcol354 355    def get_display_widget(self, size: tuple[int] | tuple[()]) -> Divider | Pile:356        """357        Arrange the cells into columns (and possibly a pile) for358        display, input or to calculate rows, and update the display359        widget.360        """361        maxcol = self._get_maxcol(size)362 363        # use cache if possible364        if self._cache_maxcol == maxcol:365            return self._w366 367        self._cache_maxcol = maxcol368        self._w = self.generate_display_widget((maxcol,))369 370        return self._w371 372    def generate_display_widget(self, size: tuple[int] | tuple[()]) -> Divider | Pile:373        """374        Actually generate display widget (ignoring cache)375        """376        maxcol = self._get_maxcol(size)377 378        divider = Divider()379        if not self.contents:380            return divider381 382        if self.v_sep > 1:383            # increase size of divider384            divider.top = self.v_sep - 1385 386        c = None387        p = Pile([])388        used_space = 0389 390        for i, (w, (_width_type, width_amount)) in enumerate(self.contents):391            if c is None or maxcol - used_space < width_amount:392                # starting a new row393                if self.v_sep:394                    p.contents.append((divider, p.options()))395                c = Columns([], self.h_sep)396                column_focused = False397                pad = Padding(c, self.align)398                # extra attribute to reference contents position399                pad.first_position = i400                p.contents.append((pad, p.options()))401 402            # Use width == maxcol in case of maxcol < width amount403            # Columns will use empty widget in case of GIVEN width > maxcol404            c.contents.append((w, c.options(WHSettings.GIVEN, min(width_amount, maxcol))))405            if (i == self.focus_position) or (not column_focused and w.selectable()):406                c.focus_position = len(c.contents) - 1407                column_focused = True408            if i == self.focus_position:409                p.focus_position = len(p.contents) - 1410            used_space = sum(x[1][1] for x in c.contents) + self.h_sep * len(c.contents)411            pad.width = used_space - self.h_sep412 413        if self.v_sep:414            # remove first divider415            del p.contents[:1]416        else:417            # Ensure p __selectable is updated418            p._contents_modified()  # pylint: disable=protected-access419 420        return p421 422    def _set_focus_from_display_widget(self) -> None:423        """424        Set the focus to the item in focus in the display widget.425        """426        # display widget (self._w) is always built as:427        #428        # Pile([429        #     Padding(430        #         Columns([ # possibly431        #         cell, ...])),432        #     Divider(), # possibly433        #     ...])434 435        pile_focus = self._w.focus436        if not pile_focus:437            return438        c = pile_focus.base_widget439        if c.focus:440            col_focus_position = c.focus_position441        else:442            col_focus_position = 0443        # pad.first_position was set by generate_display_widget() above444        self.focus_position = pile_focus.first_position + col_focus_position445 446    def keypress(447        self,448        size: tuple[int] | tuple[()],  # type: ignore[override]449        key: str,450    ) -> str | None:451        """452        Pass keypress to display widget for handling.453        Captures focus changes.454        """455        self.get_display_widget(size)456 457        if (key := super().keypress(size, key)) is not None:458            return key459 460        self._set_focus_from_display_widget()461        return None462 463    def pack(464        self,465        size: tuple[int] | tuple[()] = (),  # type: ignore[override]466        focus: bool = False,467    ) -> tuple[int, int]:468        if size:469            return super().pack(size, focus)470        if self:471            cols = len(self) * self.cell_width + (len(self) - 1) * self.h_sep472        else:473            cols = 0474        return cols, self.rows((cols,), focus)475 476    def rows(self, size: tuple[int], focus: bool = False) -> int:477        self.get_display_widget(size)478        return super().rows(size, focus=focus)479 480    def render(481        self,482        size: tuple[int] | tuple[()],  # type: ignore[override]483        focus: bool = False,484    ):485        self.get_display_widget(size)486        return super().render(size, focus)487 488    def get_cursor_coords(self, size: tuple[int] | tuple[()]) -> tuple[int, int]:489        """Get cursor from display widget."""490        self.get_display_widget(size)491        return super().get_cursor_coords(size)492 493    def move_cursor_to_coords(self, size: tuple[int] | tuple[()], col: int, row: int):494        """Set the widget in focus based on the col + row."""495        self.get_display_widget(size)496        rval = super().move_cursor_to_coords(size, col, row)497        self._set_focus_from_display_widget()498        return rval499 500    def mouse_event(501        self,502        size: tuple[int] | tuple[()],  # type: ignore[override]503        event: str,504        button: int,505        col: int,506        row: int,507        focus: bool,508    ) -> Literal[True]:509        self.get_display_widget(size)510        super().mouse_event(size, event, button, col, row, focus)511        self._set_focus_from_display_widget()512        return True  # at a minimum we adjusted our focus513 514    def get_pref_col(self, size: tuple[int] | tuple[()]):515        """Return pref col from display widget."""516        self.get_display_widget(size)517        return super().get_pref_col(size)518 
codekingpro/portable-devtools · Team Ai