Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
listbox.py2014 linesDownload Raw Back to widget
1# Urwid listbox class2#    Copyright (C) 2004-2012  Ian Ward3#4#    This library is free software; you can redistribute it and/or5#    modify it under the terms of the GNU Lesser General Public6#    License as published by the Free Software Foundation; either7#    version 2.1 of the License, or (at your option) any later version.8#9#    This library is distributed in the hope that it will be useful,10#    but WITHOUT ANY WARRANTY; without even the implied warranty of11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU12#    Lesser General Public License for more details.13#14#    You should have received a copy of the GNU Lesser General Public15#    License along with this library; if not, write to the Free Software16#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA17#18# Urwid web site: https://urwid.org/19 20 21from __future__ import annotations22 23import operator24import typing25import warnings26from collections.abc import Iterable, Sized27from contextlib import suppress28 29from urwid import signals30from urwid.canvas import CanvasCombine, SolidCanvas31 32from .constants import Sizing, VAlign, WHSettings, normalize_valign33from .container import WidgetContainerMixin34from .filler import calculate_top_bottom_filler35from .monitored_list import MonitoredFocusList, MonitoredList36from .widget import Widget, nocache_widget_render_instance37 38if typing.TYPE_CHECKING:39    from collections.abc import Callable, Hashable40 41    from typing_extensions import Literal, Self42 43    from urwid.canvas import Canvas, CompositeCanvas44 45__all__ = (46    "ListBox",47    "ListBoxError",48    "ListWalker",49    "ListWalkerError",50    "SimpleFocusListWalker",51    "SimpleListWalker",52    "VisibleInfo",53    "VisibleInfoFillItem",54    "VisibleInfoMiddle",55    "VisibleInfoTopBottom",56)57 58_T = typing.TypeVar("_T")59_K = typing.TypeVar("_K")60 61 62class ListWalkerError(Exception):63    pass64 65 66@typing.runtime_checkable67class ScrollSupportingBody(typing.Protocol):68    """Protocol for ListWalkers."""69 70    def get_focus(self) -> tuple[Widget, _K]: ...71 72    def set_focus(self, position: _K) -> None: ...73 74    def get_next(self, position: _K) -> tuple[Widget, _K] | tuple[None, None]: ...75 76    def get_prev(self, position: _K) -> tuple[Widget, _K] | tuple[None, None]: ...77 78 79@typing.runtime_checkable80class EstimatedSized(typing.Protocol):81    """Widget can estimate it's size.82 83    PEP 424 defines API for memory-efficiency.84    For the ListBox it's a sign of the limited body length.85    The main use-case is lazy-load, where real length calculation is expensive.86    """87 88    def __length_hint__(self) -> int: ...89 90 91class ListWalker(metaclass=signals.MetaSignals):  # pylint: disable=no-member, unsubscriptable-object92    # mixin not named as mixin93    signals: typing.ClassVar[list[str]] = ["modified"]94 95    def _modified(self) -> None:96        signals.emit_signal(self, "modified")97 98    def get_focus(self):99        """100        This default implementation relies on a focus attribute and a101        __getitem__() method defined in a subclass.102 103        Override and don't call this method if these are not defined.104        """105        try:106            focus = self.focus107            return self[focus], focus108        except (IndexError, KeyError, TypeError):109            return None, None110 111    def get_next(self, position):112        """113        This default implementation relies on a next_position() method and a114        __getitem__() method defined in a subclass.115 116        Override and don't call this method if these are not defined.117        """118        try:119            position = self.next_position(position)120            return self[position], position121        except (IndexError, KeyError):122            return None, None123 124    def get_prev(self, position):125        """126        This default implementation relies on a prev_position() method and a127        __getitem__() method defined in a subclass.128 129        Override and don't call this method if these are not defined.130        """131        try:132            position = self.prev_position(position)133            return self[position], position134        except (IndexError, KeyError):135            return None, None136 137 138class SimpleListWalker(MonitoredList[_T], ListWalker):139    def __init__(self, contents: Iterable[_T], wrap_around: bool = False) -> None:140        """141        contents -- list to copy into this object142 143        wrap_around -- if true, jumps to beginning/end of list on move144 145        This class inherits :class:`MonitoredList` which means146        it can be treated as a list.147 148        Changes made to this object (when it is treated as a list) are149        detected automatically and will cause ListBox objects using150        this list walker to be updated.151        """152        if not isinstance(contents, Iterable):153            raise ListWalkerError(f"SimpleListWalker expecting list like object, got: {contents!r}")154        MonitoredList.__init__(self, contents)155        self.focus = 0156        self.wrap_around = wrap_around157 158    @property159    def contents(self) -> Self:160        """161        Return self.162 163        Provides compatibility with old SimpleListWalker class.164        """165        return self166 167    def _modified(self) -> None:168        if self.focus >= len(self):169            self.focus = max(0, len(self) - 1)170        ListWalker._modified(self)171 172    def set_modified_callback(self, callback: Callable[[], typing.Any]) -> typing.NoReturn:173        """174        This function inherited from MonitoredList is not implemented in SimpleListWalker.175 176        Use connect_signal(list_walker, "modified", ...) instead.177        """178        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')179 180    def set_focus(self, position: int) -> None:181        """Set focus position."""182 183        if not 0 <= position < len(self):184            raise IndexError(f"No widget at position {position}")185 186        self.focus = position187        self._modified()188 189    def next_position(self, position: int) -> int:190        """191        Return position after start_from.192        """193        if len(self) - 1 <= position:194            if self.wrap_around:195                return 0196            raise IndexError197        return position + 1198 199    def prev_position(self, position: int) -> int:200        """201        Return position before start_from.202        """203        if position <= 0:204            if self.wrap_around:205                return len(self) - 1206            raise IndexError207        return position - 1208 209    def positions(self, reverse: bool = False) -> Iterable[int]:210        """211        Optional method for returning an iterable of positions.212        """213        if reverse:214            return range(len(self) - 1, -1, -1)215        return range(len(self))216 217 218class SimpleFocusListWalker(ListWalker, MonitoredFocusList[_T]):219    def __init__(self, contents: Iterable[_T], wrap_around: bool = False) -> None:220        """221        contents -- list to copy into this object222 223        wrap_around -- if true, jumps to beginning/end of list on move224 225        This class inherits :class:`MonitoredList` which means226        it can be treated as a list.227 228        Changes made to this object (when it is treated as a list) are229        detected automatically and will cause ListBox objects using230        this list walker to be updated.231 232        Also, items added or removed before the widget in focus with233        normal list methods will cause the focus to be updated234        intelligently.235        """236        if not isinstance(contents, Iterable):237            raise ListWalkerError(f"SimpleFocusListWalker expecting iterable object, got: {contents!r}")238        MonitoredFocusList.__init__(self, contents)239        self.wrap_around = wrap_around240 241    def set_modified_callback(self, callback: typing.Any) -> typing.NoReturn:242        """243        This function inherited from MonitoredList is not244        implemented in SimpleFocusListWalker.245 246        Use connect_signal(list_walker, "modified", ...) instead.247        """248        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')249 250    def set_focus(self, position: int) -> None:251        """Set focus position."""252        self.focus = position253        self._modified()254 255    def next_position(self, position: int) -> int:256        """257        Return position after start_from.258        """259        if len(self) - 1 <= position:260            if self.wrap_around:261                return 0262            raise IndexError263        return position + 1264 265    def prev_position(self, position: int) -> int:266        """267        Return position before start_from.268        """269        if position <= 0:270            if self.wrap_around:271                return len(self) - 1272            raise IndexError273        return position - 1274 275    def positions(self, reverse: bool = False) -> Iterable[int]:276        """277        Optional method for returning an iterable of positions.278        """279        if reverse:280            return range(len(self) - 1, -1, -1)281        return range(len(self))282 283 284class ListBoxError(Exception):285    pass286 287 288class VisibleInfoMiddle(typing.NamedTuple):289    """Named tuple for ListBox internals."""290 291    offset: int292    focus_widget: Widget293    focus_pos: Hashable294    focus_rows: int295    cursor: tuple[int, int] | tuple[int] | None296 297 298class VisibleInfoFillItem(typing.NamedTuple):299    """Named tuple for ListBox internals."""300 301    widget: Widget302    position: Hashable303    rows: int304 305 306class VisibleInfoTopBottom(typing.NamedTuple):307    """Named tuple for ListBox internals."""308 309    trim: int310    fill: list[VisibleInfoFillItem]311 312    @classmethod313    def from_raw_data(314        cls,315        trim: int,316        fill: Iterable[tuple[Widget, Hashable, int]],317    ) -> Self:318        """Construct from not typed data.319 320        Useful for overridden cases."""321        return cls(trim=trim, fill=[VisibleInfoFillItem(*item) for item in fill])  # pragma: no cover322 323 324class VisibleInfo(typing.NamedTuple):325    middle: VisibleInfoMiddle326    top: VisibleInfoTopBottom327    bottom: VisibleInfoTopBottom328 329    @classmethod330    def from_raw_data(331        cls,332        middle: tuple[int, Widget, Hashable, int, tuple[int, int] | tuple[int] | None],333        top: tuple[int, Iterable[tuple[Widget, Hashable, int]]],334        bottom: tuple[int, Iterable[tuple[Widget, Hashable, int]]],335    ) -> Self:336        """Construct from not typed data.337 338        Useful for overridden cases.339        """340        return cls(  # pragma: no cover341            middle=VisibleInfoMiddle(*middle),342            top=VisibleInfoTopBottom.from_raw_data(*top),343            bottom=VisibleInfoTopBottom.from_raw_data(*bottom),344        )345 346 347class ListBox(Widget, WidgetContainerMixin):348    """349    Vertically stacked list of widgets350    """351 352    _selectable = True353    _sizing = frozenset([Sizing.BOX])354 355    def __init__(self, body: ListWalker | Iterable[Widget]) -> None:356        """357        :param body: a ListWalker subclass such as :class:`SimpleFocusListWalker`358            that contains widgets to be displayed inside the list box359        :type body: ListWalker360        """361        super().__init__()362        if getattr(body, "get_focus", None):363            self._body: ListWalker = body364        else:365            self._body = SimpleListWalker(body)366 367        self.body = self._body  # Initialization hack368 369        # offset_rows is the number of rows between the top of the view370        # and the top of the focused item371        self.offset_rows = 0372        # inset_fraction is used when the focused widget is off the373        # top of the view.  it is the fraction of the widget cut off374        # at the top.  (numerator, denominator)375        self.inset_fraction = (0, 1)376 377        # pref_col is the preferred column for the cursor when moving378        # between widgets that use the cursor (edit boxes etc.)379        self.pref_col = "left"380 381        # variable for delayed focus change used by set_focus382        self.set_focus_pending = "first selectable"383 384        # variable for delayed valign change used by set_focus_valign385        self.set_focus_valign_pending = None386 387        # used for scrollable protocol388        self._rows_max_cached = 0389        self._rendered_size = 0, 0390 391    @property392    def body(self) -> ListWalker:393        """394        a ListWalker subclass such as :class:`SimpleFocusListWalker` that contains395        widgets to be displayed inside the list box396        """397        return self._body398 399    @body.setter400    def body(self, body: Iterable[Widget] | ListWalker) -> None:401        with suppress(AttributeError):402            signals.disconnect_signal(self._body, "modified", self._invalidate)403            # _body may be not yet assigned404 405        if getattr(body, "get_focus", None):406            self._body = body407        else:408            self._body = SimpleListWalker(body)409        try:410            signals.connect_signal(self._body, "modified", self._invalidate)411        except NameError:412            # our list walker has no modified signal so we must not413            # cache our canvases because we don't know when our414            # content has changed415            self.render = nocache_widget_render_instance(self)416        self._invalidate()417 418    @property419    def __len__(self) -> Callable[[], int]:420        if isinstance(self._body, Sized):421            return self._body.__len__422        raise AttributeError(f"{self._body.__class__.__name__} is not Sized")423 424    @property425    def __length_hint__(self) -> Callable[[], int]:  # pylint: disable=invalid-length-hint-returned426        if isinstance(self._body, (Sized, EstimatedSized)):427            return lambda: operator.length_hint(self._body)428        raise AttributeError(f'{self._body.__class__.__name__} is not Sized and do not implement "__length_hint__"')429 430    def calculate_visible(431        self,432        size: tuple[int, int],433        focus: bool = False,434    ) -> VisibleInfo | tuple[None, None, None]:435        """436        Returns the widgets that would be displayed in437        the ListBox given the current *size* and *focus*.438 439        see :meth:`Widget.render` for parameter details440 441        :returns: (*middle*, *top*, *bottom*) or (``None``, ``None``, ``None``)442 443        *middle*444            (*row offset*(when +ve) or *inset*(when -ve),445            *focus widget*, *focus position*, *focus rows*,446            *cursor coords* or ``None``)447        *top*448            (*# lines to trim off top*,449            list of (*widget*, *position*, *rows*) tuples above focus in order from bottom to top)450        *bottom*451            (*# lines to trim off bottom*,452            list of (*widget*, *position*, *rows*) tuples below focus in order from top to bottom)453        """454        (maxcol, maxrow) = size455 456        # 0. set the focus if a change is pending457        if self.set_focus_pending or self.set_focus_valign_pending:458            self._set_focus_complete((maxcol, maxrow), focus)459 460        # 1. start with the focus widget461        focus_widget, focus_pos = self._body.get_focus()462        if focus_widget is None:  # list box is empty?463            return None, None, None464        top_pos = focus_pos465 466        offset_rows, inset_rows = self.get_focus_offset_inset((maxcol, maxrow))467        #    force at least one line of focus to be visible468        if maxrow and offset_rows >= maxrow:469            offset_rows = maxrow - 1470 471        #    adjust position so cursor remains visible472        cursor = None473        if maxrow and focus_widget.selectable() and focus and hasattr(focus_widget, "get_cursor_coords"):474            cursor = focus_widget.get_cursor_coords((maxcol,))475 476        if cursor is not None:477            _cx, cy = cursor478            effective_cy = cy + offset_rows - inset_rows479 480            if effective_cy < 0:  # cursor above top?481                inset_rows = cy482            elif effective_cy >= maxrow:  # cursor below bottom?483                offset_rows = maxrow - cy - 1484                if offset_rows < 0:  # need to trim the top485                    inset_rows, offset_rows = -offset_rows, 0486 487        #    set trim_top by focus trimmimg488        trim_top = inset_rows489        focus_rows = focus_widget.rows((maxcol,), True)490 491        # 2. collect the widgets above the focus492        pos = focus_pos493        fill_lines = offset_rows494        fill_above = []495        top_pos = pos496        while fill_lines > 0:497            prev, pos = self._body.get_prev(pos)498            if prev is None:  # run out of widgets above?499                offset_rows -= fill_lines500                break501            top_pos = pos502 503            p_rows = prev.rows((maxcol,))504            if p_rows:  # filter out 0-height widgets505                fill_above.append(VisibleInfoFillItem(prev, pos, p_rows))506            if p_rows > fill_lines:  # crosses top edge?507                trim_top = p_rows - fill_lines508                break509            fill_lines -= p_rows510 511        trim_bottom = max(focus_rows + offset_rows - inset_rows - maxrow, 0)512 513        # 3. collect the widgets below the focus514        pos = focus_pos515        fill_lines = maxrow - focus_rows - offset_rows + inset_rows516        fill_below = []517        while fill_lines > 0:518            next_pos, pos = self._body.get_next(pos)519            if next_pos is None:  # run out of widgets below?520                break521 522            n_rows = next_pos.rows((maxcol,))523            if n_rows:  # filter out 0-height widgets524                fill_below.append(VisibleInfoFillItem(next_pos, pos, n_rows))525            if n_rows > fill_lines:  # crosses bottom edge?526                trim_bottom = n_rows - fill_lines527                fill_lines -= n_rows528                break529            fill_lines -= n_rows530 531        # 4. fill from top again if necessary & possible532        fill_lines = max(0, fill_lines)533 534        if fill_lines > 0 and trim_top > 0:535            if fill_lines <= trim_top:536                trim_top -= fill_lines537                offset_rows += fill_lines538                fill_lines = 0539            else:540                fill_lines -= trim_top541                offset_rows += trim_top542                trim_top = 0543        pos = top_pos544        while fill_lines > 0:545            prev, pos = self._body.get_prev(pos)546            if prev is None:547                break548 549            p_rows = prev.rows((maxcol,))550            fill_above.append(VisibleInfoFillItem(prev, pos, p_rows))551            if p_rows > fill_lines:  # more than required552                trim_top = p_rows - fill_lines553                offset_rows += fill_lines554                break555            fill_lines -= p_rows556            offset_rows += p_rows557 558        # 5. return the interesting bits559        return VisibleInfo(560            VisibleInfoMiddle(offset_rows - inset_rows, focus_widget, focus_pos, focus_rows, cursor),561            VisibleInfoTopBottom(trim_top, fill_above),562            VisibleInfoTopBottom(trim_bottom, fill_below),563        )564 565    def _check_support_scrolling(self) -> None:566        from .treetools import TreeWalker567 568        if not isinstance(self._body, ScrollSupportingBody):569            raise ListBoxError(f"{self} body do not implement methods required for scrolling protocol")570 571        if not isinstance(self._body, (Sized, EstimatedSized, TreeWalker)):572            raise ListBoxError(573                f"{self} body is not a Sized, can not estimate it's size and not a TreeWalker."574                f"Scroll is not allowed due to risk of infinite cycle of widgets load."575            )576 577        if getattr(self._body, "wrap_around", False):578            raise ListBoxError("Body is wrapped around. Scroll position calculation is undefined.")579 580    def get_scrollpos(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:581        """Current scrolling position."""582        self._check_support_scrolling()583 584        if not self._body:585            return 0586 587        if size is not None:588            self._rendered_size = size589 590        mid, top, _bottom = self.calculate_visible(self._rendered_size, focus)591 592        start_row = top.trim593        maxcol = self._rendered_size[0]594 595        if top.fill:596            pos = top.fill[-1].position597        else:598            pos = mid.focus_pos599 600        prev, pos = self._body.get_prev(pos)601        while prev is not None:602            start_row += prev.rows((maxcol,))603            prev, pos = self._body.get_prev(pos)604 605        return start_row606 607    def rows_max(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:608        """Scrollable protocol for sized iterable and not wrapped around contents."""609        self._check_support_scrolling()610 611        if size is not None:612            self._rendered_size = size613 614        if size or not self._rows_max_cached:615            cols = self._rendered_size[0]616            rows = 0617 618            focused_w, idx = self.body.get_focus()619            if focused_w:620                rows += focused_w.rows((cols,), focus)621 622                prev, pos = self._body.get_prev(idx)623                while prev is not None:624                    rows += prev.rows((cols,), False)625                    prev, pos = self._body.get_prev(pos)626 627                next_, pos = self.body.get_next(idx)628                while next_ is not None:629                    rows += next_.rows((cols,), True)630                    next_, pos = self._body.get_next(pos)631 632            self._rows_max_cached = rows633 634        return self._rows_max_cached635 636    def require_relative_scroll(self, size: tuple[int, int], focus: bool = False) -> bool:637        """Widget require relative scroll due to performance limitations of real lines count calculation."""638        return isinstance(self._body, (Sized, EstimatedSized)) and (size[1] * 3 < operator.length_hint(self.body))639 640    def get_first_visible_pos(self, size: tuple[int, int], focus: bool = False) -> int:641        self._check_support_scrolling()642 643        if not self._body:644            return 0645 646        _mid, top, _bottom = self.calculate_visible(size, focus)647        if top.fill:648            first_pos = top.fill[-1].position649        else:650            first_pos = self.focus_position651 652        over = 0653        _widget, first_pos = self.body.get_prev(first_pos)654        while first_pos is not None:655            over += 1656            _widget, first_pos = self.body.get_prev(first_pos)657 658        return over659 660    def get_visible_amount(self, size: tuple[int, int], focus: bool = False) -> int:661        self._check_support_scrolling()662 663        if not self._body:664            return 1665 666        _mid, top, bottom = self.calculate_visible(size, focus)667        return 1 + len(top.fill) + len(bottom.fill)668 669    def render(670        self,671        size: tuple[int, int],  # type: ignore[override]672        focus: bool = False,673    ) -> CompositeCanvas | SolidCanvas:674        """675        Render ListBox and return canvas.676 677        see :meth:`Widget.render` for details678        """679        (maxcol, maxrow) = size680 681        self._rendered_size = size682 683        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)684        if middle is None:685            return SolidCanvas(" ", maxcol, maxrow)686 687        _ignore, focus_widget, focus_pos, focus_rows, cursor = middle  # pylint: disable=unpacking-non-sequence688        trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence689        trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence690 691        combinelist: list[tuple[Canvas, int, bool]] = []692        rows = 0693        fill_above.reverse()  # fill_above is in bottom-up order694        for widget, w_pos, w_rows in fill_above:695            canvas = widget.render((maxcol,))696            if w_rows != canvas.rows():697                raise ListBoxError(698                    f"Widget {widget!r} at position {w_pos!r} "699                    f"within listbox calculated {w_rows:d} rows "700                    f"but rendered {canvas.rows():d}!"701                )702            rows += w_rows703            combinelist.append((canvas, w_pos, False))704 705        focus_canvas = focus_widget.render((maxcol,), focus=focus)706 707        if focus_canvas.rows() != focus_rows:708            raise ListBoxError(709                f"Focus Widget {focus_widget!r} at position {focus_pos!r} "710                f"within listbox calculated {focus_rows:d} rows "711                f"but rendered {focus_canvas.rows():d}!"712            )713        c_cursor = focus_canvas.cursor714        if cursor is not None and cursor != c_cursor:715            raise ListBoxError(716                f"Focus Widget {focus_widget!r} at position {focus_pos!r} "717                f"within listbox calculated cursor coords {cursor!r} "718                f"but rendered cursor coords {c_cursor!r}!"719            )720 721        rows += focus_rows722        combinelist.append((focus_canvas, focus_pos, True))723 724        for widget, w_pos, w_rows in fill_below:725            canvas = widget.render((maxcol,))726            if w_rows != canvas.rows():727                raise ListBoxError(728                    f"Widget {widget!r} at position {w_pos!r} "729                    f"within listbox calculated {w_rows:d} "730                    f"rows but rendered {canvas.rows():d}!"731                )732            rows += w_rows733            combinelist.append((canvas, w_pos, False))734 735        final_canvas = CanvasCombine(combinelist)736 737        if trim_top:738            final_canvas.trim(trim_top)739            rows -= trim_top740        if trim_bottom:741            final_canvas.trim_end(trim_bottom)742            rows -= trim_bottom743 744        if rows > maxrow:745            raise ListBoxError(746                f"Listbox contents too long!\nRender top={top!r}, middle={middle!r}, bottom={bottom!r}\n"747            )748 749        if rows < maxrow:750            if trim_bottom != 0:751                raise ListBoxError(752                    f"Listbox contents too short!\n"753                    f"Render top={top!r}, middle={middle!r}, bottom={bottom!r}\n"754                    f"Trim bottom={trim_bottom!r}"755                )756 757            bottom_pos = focus_pos758            if fill_below:759                bottom_pos = fill_below[-1][1]760 761            rendered_positions = frozenset(idx for _, idx, _ in combinelist)762            widget, next_pos = self._body.get_next(bottom_pos)763            while all(764                (765                    widget is not None,766                    next_pos is not None,767                    next_pos not in rendered_positions,768                )769            ):770                if widget.rows((maxcol,), False):771                    raise ListBoxError(772                        f"Listbox contents too short!\n"773                        f"Render top={top!r}, middle={middle!r}, bottom={bottom!r}\n"774                        f"Not rendered not empty widgets available (first is {widget!r} with position {next_pos!r})"775                    )776 777                widget, next_next_pos = self._body.get_next(next_pos)778                if next_pos == next_next_pos:779                    raise ListBoxError(780                        f"Next position after {next_pos!r} is invalid (points to itself)\n"781                        f"Looks like bug with {self._body!r}"782                    )783                next_pos = next_next_pos784 785            final_canvas.pad_trim_top_bottom(0, maxrow - rows)786 787        return final_canvas788 789    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:790        """791        See :meth:`Widget.get_cursor_coords` for details792        """793        (maxcol, maxrow) = size794 795        middle, _top, _bottom = self.calculate_visible((maxcol, maxrow), True)796        if middle is None:797            return None798 799        offset_inset, _ignore1, _ignore2, _ignore3, cursor = middle  # pylint: disable=unpacking-non-sequence800        if not cursor:801            return None802 803        x, y = cursor804        y += offset_inset805        if y < 0 or y >= maxrow:806            return None807        return (x, y)808 809    def set_focus_valign(810        self,811        valign: Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int],812    ):813        """Set the focus widget's display offset and inset.814 815        :param valign: one of: 'top', 'middle', 'bottom' ('relative', percentage 0=top 100=bottom)816        """817        vt, va = normalize_valign(valign, ListBoxError)818        self.set_focus_valign_pending = vt, va819 820    def set_focus(self, position, coming_from: Literal["above", "below"] | None = None) -> None:821        """822        Set the focus position and try to keep the old focus in view.823 824        :param position: a position compatible with :meth:`self._body.set_focus`825        :param coming_from: set to 'above' or 'below' if you know that826                            old position is above or below the new position.827        :type coming_from: str828        """829        if coming_from not in {"above", "below", None}:830            raise ListBoxError(f"coming_from value invalid: {coming_from!r}")831        focus_widget, focus_pos = self._body.get_focus()832        if focus_widget is None:833            raise IndexError("Can't set focus, ListBox is empty")834 835        self.set_focus_pending = coming_from, focus_widget, focus_pos836        self._body.set_focus(position)837 838    def get_focus(self):839        """840        Return a `(focus widget, focus position)` tuple, for backwards841        compatibility. You may also use the new standard container842        properties :attr:`focus` and :attr:`focus_position` to read these values.843        """844        warnings.warn(845            "only for backwards compatibility."846            "You may also use the new standard container property `focus` to get the focus "847            "and property `focus_position` to read these values."848            "API will be removed in version 5.0.",849            DeprecationWarning,850            stacklevel=2,851        )852        return self._body.get_focus()853 854    @property855    def focus(self) -> Widget | None:856        """857        the child widget in focus or None when ListBox is empty.858 859        Return the widget in focus according to our :obj:`list walker <ListWalker>`.860        """861        return self._body.get_focus()[0]862 863    def _get_focus_position(self):864        """865        Return the list walker position of the widget in focus. The type866        of value returned depends on the :obj:`list walker <ListWalker>`.867 868        """869        w, pos = self._body.get_focus()870        if w is None:871            raise IndexError("No focus_position, ListBox is empty")872        return pos873 874    focus_position = property(875        _get_focus_position,876        set_focus,877        doc="""878        the position of child widget in focus. The valid values for this879        position depend on the list walker in use.880        :exc:`IndexError` will be raised by reading this property when the881        ListBox is empty or setting this property to an invalid position.882        """,883    )884 885    def _contents(self):886        # noinspection PyMethodParameters887        class ListBoxContents:888            # pylint: disable=no-self-argument889 890            __getitem__ = self._contents__getitem__891 892            __len__ = self.__len__893 894            def __repr__(inner_self) -> str:895                return f"<{inner_self.__class__.__name__} for {self!r} at 0x{id(inner_self):X}>"896 897            def __call__(inner_self) -> Self:898                warnings.warn(899                    "ListBox.contents is a property, not a method",900                    DeprecationWarning,901                    stacklevel=3,902                )903                return inner_self904 905        return ListBoxContents()906 907    def _contents__getitem__(self, key):908        # try list walker protocol v2 first909        if hasattr(self._body, "__getitem__"):910            try:911                return (self._body[key], None)912            except (IndexError, KeyError) as exc:913                raise KeyError(f"ListBox.contents key not found: {key!r}").with_traceback(exc.__traceback__) from exc914        # fall back to v1915        _w, old_focus = self._body.get_focus()916 917        try:918            self._body.set_focus(key)919            return self._body.get_focus()[0]920        except (IndexError, KeyError) as exc:921            raise KeyError(f"ListBox.contents key not found: {key!r}").with_traceback(exc.__traceback__) from exc922        finally:923            self._body.set_focus(old_focus)924 925    @property926    def contents(self):927        """928        An object that allows reading widgets from the ListBox's list929        walker as a `(widget, options)` tuple. `None` is currently the only930        value for options.931 932        .. warning::933 934            This object may not be used to set or iterate over contents.935 936            You must use the list walker stored as937            :attr:`.body` to perform manipulation and iteration, if supported.938        """939        return self._contents()940 941    def options(self):942        """943        There are currently no options for ListBox contents.944 945        Return None as a placeholder for future options.946        """947 948    def _set_focus_valign_complete(self, size: tuple[int, int], focus: bool) -> None:949        """Finish setting the offset and inset now that we have have a maxcol & maxrow."""950        (maxcol, maxrow) = size951        vt, va = self.set_focus_valign_pending952        self.set_focus_valign_pending = None953        self.set_focus_pending = None954 955        focus_widget, _focus_pos = self._body.get_focus()956        if focus_widget is None:957            return958 959        rows = focus_widget.rows((maxcol,), focus)960        rtop, _rbot = calculate_top_bottom_filler(961            maxrow,962            vt,963            va,964            WHSettings.GIVEN,965            rows,966            None,967            0,968            0,969        )970 971        self.shift_focus((maxcol, maxrow), rtop)972 973    def _set_focus_first_selectable(self, size: tuple[int, int], focus: bool) -> None:974        """Choose the first visible, selectable widget below the current focus as the focus widget."""975        (maxcol, maxrow) = size976        self.set_focus_valign_pending = None977        self.set_focus_pending = None978        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)979        if middle is None:980            return981 982        row_offset, focus_widget, _focus_pos, focus_rows, _cursor = middle  # pylint: disable=unpacking-non-sequence983        _trim_top, _fill_above = top  # pylint: disable=unpacking-non-sequence984        trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence985 986        if focus_widget.selectable():987            return988 989        if trim_bottom:990            fill_below = fill_below[:-1]991        new_row_offset = row_offset + focus_rows992        for widget, pos, rows in fill_below:993            if widget.selectable():994                self._body.set_focus(pos)995                self.shift_focus((maxcol, maxrow), new_row_offset)996                return997            new_row_offset += rows998 999    def _set_focus_complete(self, size: tuple[int, int], focus: bool) -> None:1000        """Finish setting the position now that we have maxcol & maxrow."""1001        (maxcol, maxrow) = size1002        self._invalidate()1003        if self.set_focus_pending == "first selectable":1004            return self._set_focus_first_selectable((maxcol, maxrow), focus)1005        if self.set_focus_valign_pending is not None:1006            return self._set_focus_valign_complete((maxcol, maxrow), focus)1007        coming_from, _focus_widget, focus_pos = self.set_focus_pending1008        self.set_focus_pending = None1009 1010        # new position1011        _new_focus_widget, position = self._body.get_focus()1012        if focus_pos == position:1013            # do nothing1014            return None1015 1016        # restore old focus temporarily1017        self._body.set_focus(focus_pos)1018 1019        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus)1020        if middle is None:1021            return None1022 1023        focus_offset, _focus_widget, focus_pos, focus_rows, _cursor = middle  # pylint: disable=unpacking-non-sequence1024        _trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence1025        _trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence1026 1027        offset = focus_offset1028        for _widget, pos, rows in fill_above:1029            offset -= rows1030            if pos == position:1031                self.change_focus((maxcol, maxrow), pos, offset, "below")1032                return None1033 1034        offset = focus_offset + focus_rows1035        for _widget, pos, rows in fill_below:1036            if pos == position:1037                self.change_focus((maxcol, maxrow), pos, offset, "above")1038                return None1039            offset += rows1040 1041        # failed to find widget among visible widgets1042        self._body.set_focus(position)1043        widget, position = self._body.get_focus()1044        rows = widget.rows((maxcol,), focus)1045 1046        if coming_from == "below":1047            offset = 01048        elif coming_from == "above":1049            offset = maxrow - rows1050        else:1051            offset = (maxrow - rows) // 21052        self.shift_focus((maxcol, maxrow), offset)1053        return None1054 1055    def shift_focus(self, size: tuple[int, int], offset_inset: int) -> None:1056        """1057        Move the location of the current focus relative to the top.1058        This is used internally by methods that know the widget's *size*.1059 1060        See also :meth:`.set_focus_valign`.1061 1062        :param size: see :meth:`Widget.render` for details1063        :param offset_inset: either the number of rows between the1064            top of the listbox and the start of the focus widget (+ve1065            value) or the number of lines of the focus widget hidden off1066            the top edge of the listbox (-ve value) or ``0`` if the top edge1067            of the focus widget is aligned with the top edge of the1068            listbox.1069        :type offset_inset: int1070        """1071        (maxcol, maxrow) = size1072 1073        if offset_inset >= 0:1074            if offset_inset >= maxrow:1075                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {maxrow!r} rows in list box")1076            self.offset_rows = offset_inset1077            self.inset_fraction = (0, 1)1078        else:1079            target, _ignore = self._body.get_focus()1080            tgt_rows = target.rows((maxcol,), True)1081            if offset_inset + tgt_rows <= 0:1082                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {tgt_rows!r} rows in target!")1083            self.offset_rows = 01084            self.inset_fraction = (-offset_inset, tgt_rows)1085        self._invalidate()1086 1087    def update_pref_col_from_focus(self, size: tuple[int, int]) -> None:1088        """Update self.pref_col from the focus widget."""1089        # TODO: should this not be private?1090        (maxcol, _maxrow) = size1091 1092        widget, _old_pos = self._body.get_focus()1093        if widget is None:1094            return1095 1096        pref_col = None1097        if hasattr(widget, "get_pref_col"):1098            pref_col = widget.get_pref_col((maxcol,))1099        if pref_col is None and hasattr(widget, "get_cursor_coords"):1100            coords = widget.get_cursor_coords((maxcol,))1101            if isinstance(coords, tuple):1102                pref_col, _y = coords1103        if pref_col is not None:1104            self.pref_col = pref_col1105 1106    def change_focus(1107        self,1108        size: tuple[int, int],1109        position,1110        offset_inset: int = 0,1111        coming_from: Literal["above", "below"] | None = None,1112        cursor_coords: tuple[int, int] | None = None,1113        snap_rows: int | None = None,1114    ) -> None:1115        """1116        Change the current focus widget.1117        This is used internally by methods that know the widget's *size*.1118 1119        See also :meth:`.set_focus`.1120 1121        :param size: see :meth:`Widget.render` for details1122        :param position: a position compatible with :meth:`self._body.set_focus`1123        :param offset_inset: either the number of rows between the1124            top of the listbox and the start of the focus widget (+ve1125            value) or the number of lines of the focus widget hidden off1126            the top edge of the listbox (-ve value) or 0 if the top edge1127            of the focus widget is aligned with the top edge of the1128            listbox (default if unspecified)1129        :type offset_inset: int1130        :param coming_from: either 'above', 'below' or unspecified `None`1131        :type coming_from: str1132        :param cursor_coords: (x, y) tuple indicating the desired1133            column and row for the cursor, a (x,) tuple indicating only1134            the column for the cursor, or unspecified1135        :type cursor_coords: (int, int)1136        :param snap_rows: the maximum number of extra rows to scroll1137            when trying to "snap" a selectable focus into the view1138        :type snap_rows: int1139        """1140        (maxcol, maxrow) = size1141 1142        # update pref_col before change1143        if cursor_coords:1144            self.pref_col = cursor_coords[0]1145        else:1146            self.update_pref_col_from_focus((maxcol, maxrow))1147 1148        self._invalidate()1149        self._body.set_focus(position)1150        target, _ignore = self._body.get_focus()1151        tgt_rows = target.rows((maxcol,), True)1152        if snap_rows is None:1153            snap_rows = maxrow - 11154 1155        # "snap" to selectable widgets1156        align_top = 01157        align_bottom = maxrow - tgt_rows1158 1159        if coming_from == "above" and target.selectable() and offset_inset > align_bottom:1160            if snap_rows >= offset_inset - align_bottom:1161                offset_inset = align_bottom1162            elif snap_rows >= offset_inset - align_top:1163                offset_inset = align_top1164            else:1165                offset_inset -= snap_rows1166 1167        if coming_from == "below" and target.selectable() and offset_inset < align_top:1168            if snap_rows >= align_top - offset_inset:1169                offset_inset = align_top1170            elif snap_rows >= align_bottom - offset_inset:1171                offset_inset = align_bottom1172            else:1173                offset_inset += snap_rows1174 1175        # convert offset_inset to offset_rows or inset_fraction1176        if offset_inset >= 0:1177            self.offset_rows = offset_inset1178            self.inset_fraction = (0, 1)1179        else:1180            if offset_inset + tgt_rows <= 0:1181                raise ListBoxError(f"Invalid offset_inset: {offset_inset}, only {tgt_rows} rows in target!")1182            self.offset_rows = 01183            self.inset_fraction = (-offset_inset, tgt_rows)1184 1185        if cursor_coords is None:1186            if coming_from is None:1187                return  # must either know row or coming_from1188            cursor_coords = (self.pref_col,)1189 1190        if not hasattr(target, "move_cursor_to_coords"):1191            return1192 1193        attempt_rows = []1194 1195        if len(cursor_coords) == 1:1196            # only column (not row) specified1197            # start from closest edge and move inwards1198            (pref_col,) = cursor_coords1199            if coming_from == "above":1200                attempt_rows = range(tgt_rows)

Showing the first 1,200 of 2014 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai