Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/appui/column_chooser_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@ def __init__(
self._frozen_labels: list[Label] = []
for frozen_key in self._frozen_keys:
frozen_column = self._registry[frozen_key]
self._frozen_labels.append(
Label(frozen_column.label, classes="frozen-column-label")
frozen_label = Label(
frozen_column.label,
classes="frozen-column-label",
)
frozen_label.tooltip = frozen_column.full_name
self._frozen_labels.append(frozen_label)
self._all_keys = list(self._registry.keys())

self._available_list = ListView(classes="column-list available-list")
Expand Down Expand Up @@ -291,7 +294,9 @@ def _build_list_item(self, column_key: str) -> ListItem:
The list item widget.
"""
column = self._registry[column_key]
return ListItem(Label(column.label), id=column_key)
label = Label(column.label)
label.tooltip = column.full_name
return ListItem(label, id=column_key)

def _can_move_active(self, offset: int) -> bool:
"""Check if the active list's selected item can be moved by the given offset.
Expand Down
5 changes: 5 additions & 0 deletions src/appui/column_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def label(self) -> str:
"""Display label for the column."""
...

@property
def full_name(self) -> str:
"""Full display name for the column."""
...


@runtime_checkable
class ColumnContainer(Protocol):
Expand Down
25 changes: 25 additions & 0 deletions src/appui/enhanced_data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ class EnhancedColumn(Generic[T]):

_: KW_ONLY

full_name: str = ""
"""The full display name for the column (used for tooltips)."""

width: int = 10
"""The width of the column."""

Expand All @@ -149,6 +152,8 @@ def __post_init__(self) -> None:

if self.key is None: # pyright: ignore[reportUnnecessaryComparison]
object.__setattr__(self, "key", self.label)
if not self.full_name:
object.__setattr__(self, "full_name", self.label)
if self.cell_factory is None:
object.__setattr__(
self,
Expand Down Expand Up @@ -265,12 +270,15 @@ def clear(self, columns: bool = False) -> Self:
@override
def watch_hover_coordinate(self, old: Coordinate, value: Coordinate) -> None:
if self.is_ordering:
self._set_header_tooltip(None)
return

if value.row == -1:
self._hovered_column = value.column
self._set_header_tooltip(value.column)
else:
self._hovered_column = -1
self._set_header_tooltip(None)

super().watch_hover_coordinate(old, value)

Expand Down Expand Up @@ -416,6 +424,7 @@ def is_ordering(self, value: bool) -> None:

self._set_hover_cursor(active=not value)
if value:
self._set_header_tooltip(None)
if self._hovered_column == -1:
self._hovered_column = self._sort_column_idx
self._bindings = self._ordering_bindings
Expand Down Expand Up @@ -548,3 +557,19 @@ def _update_sort(self) -> None:
self._sort_column_key,
reverse=self._sort_direction == SortDirection.ASCENDING,
)

def _set_header_tooltip(self, column_index: int | None) -> None:
"""Set the tooltip based on the hovered column index.

Args:
column_index: The hovered column index, or None to clear the tooltip.
"""

if column_index is None:
self.tooltip = None
return
if 0 <= column_index < len(self._enhanced_columns):
column = self._enhanced_columns[column_index]
self.tooltip = column.full_name or column.label
return
self.tooltip = None
14 changes: 14 additions & 0 deletions src/appui/quote_column_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ def _get_style_for_value(value: float) -> str:
"ticker": (
quote_column(
"Ticker",
full_name="Ticker Symbol",
width=8,
key="ticker",
justification=Justify.LEFT,
Expand All @@ -215,6 +216,7 @@ def _get_style_for_value(value: float) -> str:
"last": (
quote_column(
"Last",
full_name="Market Price",
width=10,
key="last",
cell_factory=lambda q: FloatCell(
Expand All @@ -228,6 +230,7 @@ def _get_style_for_value(value: float) -> str:
"change": (
quote_column(
"Change",
full_name="Market Change",
width=10,
key="change",
cell_factory=lambda q: FloatCell(
Expand All @@ -242,6 +245,7 @@ def _get_style_for_value(value: float) -> str:
"change_percent": (
quote_column(
"Chg %",
full_name="Market Change Percent",
width=8,
key="change_percent",
cell_factory=lambda q: PercentCell(
Expand All @@ -255,6 +259,7 @@ def _get_style_for_value(value: float) -> str:
"open": (
quote_column(
"Open",
full_name="Market Open",
width=10,
key="open",
cell_factory=lambda q: FloatCell(
Expand All @@ -268,6 +273,7 @@ def _get_style_for_value(value: float) -> str:
"low": (
quote_column(
"Low",
full_name="Day Low",
width=10,
key="low",
cell_factory=lambda q: FloatCell(
Expand All @@ -281,6 +287,7 @@ def _get_style_for_value(value: float) -> str:
"high": (
quote_column(
"High",
full_name="Day High",
width=10,
key="high",
cell_factory=lambda q: FloatCell(
Expand All @@ -294,6 +301,7 @@ def _get_style_for_value(value: float) -> str:
"_52w_low": (
quote_column(
"52w Low",
full_name="52-Week Low",
width=10,
key="_52w_low",
cell_factory=lambda q: FloatCell(
Expand All @@ -307,6 +315,7 @@ def _get_style_for_value(value: float) -> str:
"_52w_high": (
quote_column(
"52w High",
full_name="52-Week High",
width=10,
key="_52w_high",
cell_factory=lambda q: FloatCell(
Expand All @@ -320,6 +329,7 @@ def _get_style_for_value(value: float) -> str:
"volume": (
quote_column(
"Volume",
full_name="Market Volume",
width=10,
key="volume",
cell_factory=lambda q: CompactNumberCell(
Expand All @@ -332,6 +342,7 @@ def _get_style_for_value(value: float) -> str:
"avg_volume": (
quote_column(
"Avg Vol",
full_name="Average Daily Volume (3 Month)",
width=10,
key="avg_volume",
cell_factory=lambda q: CompactNumberCell(
Expand All @@ -344,6 +355,7 @@ def _get_style_for_value(value: float) -> str:
"pe": (
quote_column(
"P/E",
full_name="Trailing Price-to-Earnings Ratio",
width=6,
key="pe",
cell_factory=lambda q: FloatCell(
Expand All @@ -356,6 +368,7 @@ def _get_style_for_value(value: float) -> str:
"dividend": (
quote_column(
"Div",
full_name="Dividend Yield",
width=6,
key="dividend",
cell_factory=lambda q: FloatCell(
Expand All @@ -368,6 +381,7 @@ def _get_style_for_value(value: float) -> str:
"market_cap": (
quote_column(
"Mkt Cap",
full_name="Market Capitalization",
width=10,
key="market_cap",
cell_factory=lambda q: CompactNumberCell(
Expand Down
7 changes: 6 additions & 1 deletion src/appui/quote_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ def quote_table() -> QuoteTable:
return table


def quote_column(
def quote_column( # noqa: PLR0913
label: str,
*,
full_name: str | None = None,
key: str | None = None,
width: int | None = None,
justification: Justify | None = None,
Expand All @@ -41,6 +42,8 @@ def quote_column(

Args:
label (str): The display label for the column.
full_name (str | None): The full display name of the column. Defaults to
the label when omitted.
key (str | None): The key to access the attribute in YQuote.
Defaults to None, which uses the label as the key.
width (int | None): The width of the column.
Expand All @@ -55,12 +58,14 @@ def quote_column(
class _QuoteColumnParams(TypedDict, total=False):
"""Typing helper for optional QuoteColumn parameters."""

full_name: str
key: str
width: int
justification: Justify
cell_factory: Callable[[YQuote], EnhancedTableCell]

params: _QuoteColumnParams = {}
params["full_name"] = full_name if full_name is not None else label
if key is not None:
params["key"] = key
if width is not None:
Expand Down
21 changes: 16 additions & 5 deletions tests/appui/test_column_chooser_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
class _FakeColumn(ColumnMetadata):
"""Minimal column metadata for tests."""

def __init__(self, key: str, label: str) -> None:
def __init__(self, key: str, label: str, full_name: str | None = None) -> None:
self._key = key
self._label = label
self._full_name = full_name or label

@property
def key(self) -> str:
Expand All @@ -35,6 +36,10 @@ def key(self) -> str:
def label(self) -> str:
return self._label

@property
def full_name(self) -> str:
return self._full_name


class _FakeRegistry(ColumnRegistry):
"""Ordered registry backed by a dict."""
Expand Down Expand Up @@ -148,7 +153,9 @@ def _label_text(label: Label) -> str:
async def test_build_list_item_uses_registry_label() -> None:
"""Ensure list items mirror registry labels and keys."""

registry = _FakeRegistry([_FakeColumn("alpha", "Alpha Column")])
registry = _FakeRegistry(
[_FakeColumn("alpha", "Alpha Column", full_name="Alpha Column Full")]
)
container = _FakeContainer(active=[])
app = _ColumnChooserTestApp(registry, container, DoubloonConfig())

Expand All @@ -160,7 +167,9 @@ async def test_build_list_item_uses_registry_label() -> None:
item = next(iter(screen._available_list.children))
assert isinstance(item, ListItem)
assert str(item.id) == "alpha"
assert _label_text(item.query_one(Label)) == "Alpha Column"
label = item.query_one(Label)
assert _label_text(label) == "Alpha Column"
assert label.tooltip == "Alpha Column Full"


@pytest.mark.asyncio
Expand All @@ -169,7 +178,7 @@ async def test_populate_lists_excludes_frozen_and_preserves_order() -> None:

registry = _FakeRegistry(
[
_FakeColumn("frozen", "Frozen"),
_FakeColumn("frozen", "Frozen", full_name="Frozen Column"),
_FakeColumn("first", "First"),
_FakeColumn("second", "Second"),
_FakeColumn("third", "Third"),
Expand All @@ -184,7 +193,9 @@ async def test_populate_lists_excludes_frozen_and_preserves_order() -> None:

assert _list_item_ids(screen._available_list) == ["first", "third"]
assert _list_item_ids(screen._active_list) == ["second"]
assert [_label_text(label) for label in screen._frozen_labels] == ["Frozen"]
frozen_label = screen._frozen_labels[0]
assert _label_text(frozen_label) == "Frozen"
assert frozen_label.tooltip == "Frozen Column"


@pytest.mark.ui
Expand Down
25 changes: 22 additions & 3 deletions tests/appui/test_enhanced_data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def test_enhanced_column_defaults_apply(sample_row: SampleRow) -> None:
column: EnhancedColumn[SampleRow] = EnhancedColumn(label="Name")

assert column.key == "Name"
assert column.full_name == "Name"
assert column.cell_factory is not None
cell = column.cell_factory(sample_row)
assert isinstance(cell, EnhancedTableCell)
Expand Down Expand Up @@ -199,9 +200,27 @@ def test_styled_column_label_places_arrows_correctly(
assert label_text.startswith(prefix)
assert label_text.endswith(suffix)

expected_core = column.label if not direction else column.label[: column.width - 2]
core = label_text.removeprefix(prefix).removesuffix(suffix)
assert core.strip() == expected_core.strip()

def test_header_hover_sets_tooltip_for_full_name() -> None:
"""Hovering a header cell sets and clears the tooltip."""

table: EnhancedDataTable[SampleRow] = EnhancedDataTable()
column = EnhancedColumn[SampleRow](
label="Chg",
key="change",
full_name="Market Change",
width=8,
justification=Justify.RIGHT,
)
table.add_enhanced_column(column)

table.watch_hover_coordinate(Coordinate(0, 0), Coordinate(-1, 0))

assert table.tooltip == "Market Change"

table.watch_hover_coordinate(Coordinate(-1, 0), Coordinate(0, 0))

assert table.tooltip is None


def test_get_styled_column_label_handles_sort_indicators(
Expand Down
3 changes: 3 additions & 0 deletions tests/appui/test_quote_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __str__(self) -> str:

assert isinstance(column, EnhancedColumn)
assert column.label == "Label"
assert column.full_name == "Label"
assert column.key == "Label"
assert column.cell_factory is not None
dummy_quote = cast("YQuote", DummyQuote())
Expand All @@ -49,6 +50,7 @@ def factory(q: YQuote) -> EnhancedTableCell:

column = quote_column(
"Ticker",
full_name="Ticker Symbol",
width=test_width,
key="ticker",
justification=Justify.LEFT,
Expand All @@ -59,6 +61,7 @@ def factory(q: YQuote) -> EnhancedTableCell:
assert column.key == "ticker"
assert column.justification is Justify.LEFT
assert column.cell_factory is factory
assert column.full_name == "Ticker Symbol"


def test_quote_table_factory_returns_enhanced_data_table() -> None:
Expand Down
Loading
Loading