|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import contextlib |
5 | 6 | import io |
6 | 7 | import os |
7 | 8 | from typing import TYPE_CHECKING, Any, ClassVar |
|
12 | 13 | from textual.app import App, ComposeResult |
13 | 14 | from textual.binding import Binding |
14 | 15 | from textual.containers import Horizontal, Vertical, VerticalScroll |
| 16 | +from textual.content import Content |
15 | 17 | from textual.css.query import NoMatches |
16 | 18 | from textual.screen import ModalScreen |
17 | 19 | from textual.theme import Theme |
|
26 | 28 | Static, |
27 | 29 | Tree, |
28 | 30 | ) |
| 31 | +from textual.widgets._header import HeaderTitle |
29 | 32 | from textual.widgets.option_list import Option |
30 | 33 | from textual.widgets.selection_list import Selection |
31 | 34 |
|
@@ -1526,78 +1529,68 @@ def on_progress(downloaded: int, content_total: int | None) -> None: |
1526 | 1529 |
|
1527 | 1530 |
|
1528 | 1531 | class B2ViewHeader(Header): |
1529 | | - """App header that also shows the open bundle's filename, docked left. |
1530 | | -
|
1531 | | - Adds a left-docked filename label next to the stock icon plus a right-docked |
1532 | | - spacer of the *same* width, so the title ("b2view — Python-Blosc2 X") stays |
1533 | | - centered across the full header. The two widths are recomputed on resize so |
1534 | | - the title's full text is reserved first and the filename only takes what is |
1535 | | - left over (truncating, then hiding, as the terminal narrows) — the title |
1536 | | - keeps its room as long as possible. Set the name with :meth:`set_filename`. |
| 1532 | + """App header that also shows the open bundle's filename, left of the title. |
| 1533 | +
|
| 1534 | + The filename is rendered *into the stock ``HeaderTitle`` widget* (in the |
| 1535 | + space left of the centered "b2view — Python-Blosc2 X" title) rather than as |
| 1536 | + extra docked child widgets. Adding docked children to the Header was found |
| 1537 | + to break Tab focus cycling between the panels under the Windows test driver, |
| 1538 | + so this keeps the Header's widget tree exactly as Textual builds it and only |
| 1539 | + overrides what the title renders. The filename takes only the room left over |
| 1540 | + once the centered title is reserved, truncating (with an ellipsis) as the |
| 1541 | + terminal narrows. Set the name with :meth:`set_filename`. |
1537 | 1542 | """ |
1538 | 1543 |
|
1539 | | - _ICON_WIDTH = 8 # HeaderIcon dock width |
1540 | | - _CLOCK_WIDTH = 10 # HeaderClockSpace dock width |
1541 | | - |
1542 | 1544 | _GAP = 2 # cells kept between the filename and the centered title |
1543 | 1545 |
|
1544 | 1546 | DEFAULT_CSS = """ |
1545 | | - B2ViewHeader #header-filename, B2ViewHeader #header-spacer { |
1546 | | - dock: left; |
1547 | | - text-opacity: 85%; |
1548 | | - text-style: italic; |
1549 | | - text-wrap: nowrap; |
1550 | | - text-overflow: ellipsis; |
1551 | | - content-align: left middle; |
1552 | | - } |
1553 | | - B2ViewHeader #header-spacer { |
1554 | | - dock: right; |
| 1547 | + B2ViewHeader HeaderTitle { |
| 1548 | + content-align: left middle; /* we place the title ourselves */ |
1555 | 1549 | } |
1556 | 1550 | """ |
1557 | 1551 |
|
1558 | 1552 | def __init__(self, *args, **kwargs) -> None: |
1559 | 1553 | super().__init__(*args, **kwargs) |
1560 | 1554 | self._label = "" |
1561 | 1555 |
|
1562 | | - def compose(self) -> ComposeResult: |
1563 | | - yield from super().compose() |
1564 | | - yield Static(id="header-filename") |
1565 | | - yield Static(id="header-spacer") # mirrors the filename width to keep the title centered |
1566 | | - |
1567 | 1556 | def set_filename(self, label: str) -> None: |
1568 | 1557 | self._label = label |
1569 | | - self._relayout() |
| 1558 | + self._refresh_title() |
1570 | 1559 |
|
1571 | 1560 | def on_resize(self) -> None: |
1572 | | - self._relayout() |
| 1561 | + self._refresh_title() |
| 1562 | + |
| 1563 | + def _refresh_title(self) -> None: |
| 1564 | + with contextlib.suppress(NoMatches): # HeaderTitle may not be composed yet |
| 1565 | + self.query_one(HeaderTitle).update(self.format_title()) |
1573 | 1566 |
|
1574 | | - def _relayout(self) -> None: |
1575 | | - """Size the filename + mirror spacer, reserving the title's width first. |
| 1567 | + def format_title(self) -> Content: |
| 1568 | + """Render the centered title with the filename in the left gutter. |
1576 | 1569 |
|
1577 | | - No CSS padding (the width math stays in exact cells): a leading space on |
1578 | | - the label provides the gap from the icon. |
| 1570 | + With no filename (or no room for one) this is just the stock centered |
| 1571 | + title. Otherwise the title is left-padded so it stays centered across |
| 1572 | + the ``HeaderTitle`` region, and the filename fills the left gutter. |
1579 | 1573 | """ |
| 1574 | + base = super().format_title() |
| 1575 | + if not self._label: |
| 1576 | + return base |
1580 | 1577 | try: |
1581 | | - fname = self.query_one("#header-filename", Static) |
1582 | | - spacer = self.query_one("#header-spacer", Static) |
| 1578 | + width = self.query_one(HeaderTitle).content_size.width |
1583 | 1579 | except NoMatches: |
1584 | | - return # not composed yet |
1585 | | - text = f" {self._label}" if self._label else "" |
1586 | | - fname.update(text) |
1587 | | - title = self.app.title or "" |
1588 | | - sub = self.app.sub_title or "" |
1589 | | - title_len = len(title) + (len(sub) + 3 if sub else 0) # "title — sub" |
1590 | | - # Reserve the icon, clock, the full title and a gap; split the rest |
1591 | | - # symmetrically so the title stays centered and fully visible — the |
1592 | | - # filename takes only the leftover, truncating then hiding as it tightens. |
1593 | | - budget = self.size.width - self._ICON_WIDTH - self._CLOCK_WIDTH - title_len - self._GAP |
1594 | | - each = max(0, budget // 2) |
1595 | | - fname_w = min(len(text), each) if text else 0 |
1596 | | - show = fname_w >= 2 # below this there is not even room for " x" |
1597 | | - for widget in (fname, spacer): |
1598 | | - widget.display = show |
1599 | | - if show: |
1600 | | - widget.styles.width = fname_w |
| 1580 | + return base |
| 1581 | + title_len = base.cell_length |
| 1582 | + if width <= 0 or title_len >= width: |
| 1583 | + return base # not even room for the title alone |
| 1584 | + # Left padding that centers the title across the full HeaderTitle width. |
| 1585 | + left_pad = (width - title_len) // 2 |
| 1586 | + avail = left_pad - self._GAP # cells the filename may use |
| 1587 | + if avail < 2: # below this there is not even room for " x" |
| 1588 | + return base |
| 1589 | + label = f" {self._label}" |
| 1590 | + if len(label) > avail: |
| 1591 | + label = label[: avail - 1] + "…" |
| 1592 | + gutter = " " * (left_pad - len(label)) |
| 1593 | + return Content.assemble((label, "italic dim"), gutter, base) |
1601 | 1594 |
|
1602 | 1595 |
|
1603 | 1596 | class B2ViewApp(App): |
|
0 commit comments