Skip to content

Commit afbd020

Browse files
committed
Add full-name tooltips for columns
1 parent bececef commit afbd020

9 files changed

+115
-12
lines changed

src/appui/column_chooser_screen.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ def __init__(
7575
self._frozen_labels: list[Label] = []
7676
for frozen_key in self._frozen_keys:
7777
frozen_column = self._registry[frozen_key]
78-
self._frozen_labels.append(
79-
Label(frozen_column.label, classes="frozen-column-label")
78+
frozen_label = Label(
79+
frozen_column.label,
80+
classes="frozen-column-label",
8081
)
82+
frozen_label.tooltip = frozen_column.full_name
83+
self._frozen_labels.append(frozen_label)
8184
self._all_keys = list(self._registry.keys())
8285

8386
self._available_list = ListView(classes="column-list available-list")
@@ -291,7 +294,9 @@ def _build_list_item(self, column_key: str) -> ListItem:
291294
The list item widget.
292295
"""
293296
column = self._registry[column_key]
294-
return ListItem(Label(column.label), id=column_key)
297+
label = Label(column.label)
298+
label.tooltip = column.full_name
299+
return ListItem(label, id=column_key)
295300

296301
def _can_move_active(self, offset: int) -> bool:
297302
"""Check if the active list's selected item can be moved by the given offset.

src/appui/column_protocols.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def label(self) -> str:
2222
"""Display label for the column."""
2323
...
2424

25+
@property
26+
def full_name(self) -> str:
27+
"""Full display name for the column."""
28+
...
29+
2530

2631
@runtime_checkable
2732
class ColumnContainer(Protocol):

src/appui/enhanced_data_table.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class EnhancedColumn(Generic[T]):
132132

133133
_: KW_ONLY
134134

135+
full_name: str = ""
136+
"""The full display name for the column (used for tooltips)."""
137+
135138
width: int = 10
136139
"""The width of the column."""
137140

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

150153
if self.key is None: # pyright: ignore[reportUnnecessaryComparison]
151154
object.__setattr__(self, "key", self.label)
155+
if not self.full_name:
156+
object.__setattr__(self, "full_name", self.label)
152157
if self.cell_factory is None:
153158
object.__setattr__(
154159
self,
@@ -265,12 +270,15 @@ def clear(self, columns: bool = False) -> Self:
265270
@override
266271
def watch_hover_coordinate(self, old: Coordinate, value: Coordinate) -> None:
267272
if self.is_ordering:
273+
self._set_header_tooltip(None)
268274
return
269275

270276
if value.row == -1:
271277
self._hovered_column = value.column
278+
self._set_header_tooltip(value.column)
272279
else:
273280
self._hovered_column = -1
281+
self._set_header_tooltip(None)
274282

275283
super().watch_hover_coordinate(old, value)
276284

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

417425
self._set_hover_cursor(active=not value)
418426
if value:
427+
self._set_header_tooltip(None)
419428
if self._hovered_column == -1:
420429
self._hovered_column = self._sort_column_idx
421430
self._bindings = self._ordering_bindings
@@ -548,3 +557,19 @@ def _update_sort(self) -> None:
548557
self._sort_column_key,
549558
reverse=self._sort_direction == SortDirection.ASCENDING,
550559
)
560+
561+
def _set_header_tooltip(self, column_index: int | None) -> None:
562+
"""Set the tooltip based on the hovered column index.
563+
564+
Args:
565+
column_index: The hovered column index, or None to clear the tooltip.
566+
"""
567+
568+
if column_index is None:
569+
self.tooltip = None
570+
return
571+
if 0 <= column_index < len(self._enhanced_columns):
572+
column = self._enhanced_columns[column_index]
573+
self.tooltip = column.full_name or column.label
574+
return
575+
self.tooltip = None

src/appui/quote_column_definitions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def _get_style_for_value(value: float) -> str:
204204
"ticker": (
205205
quote_column(
206206
"Ticker",
207+
full_name="Ticker Symbol",
207208
width=8,
208209
key="ticker",
209210
justification=Justify.LEFT,
@@ -215,6 +216,7 @@ def _get_style_for_value(value: float) -> str:
215216
"last": (
216217
quote_column(
217218
"Last",
219+
full_name="Market Price",
218220
width=10,
219221
key="last",
220222
cell_factory=lambda q: FloatCell(
@@ -228,6 +230,7 @@ def _get_style_for_value(value: float) -> str:
228230
"change": (
229231
quote_column(
230232
"Change",
233+
full_name="Market Change",
231234
width=10,
232235
key="change",
233236
cell_factory=lambda q: FloatCell(
@@ -242,6 +245,7 @@ def _get_style_for_value(value: float) -> str:
242245
"change_percent": (
243246
quote_column(
244247
"Chg %",
248+
full_name="Market Change Percent",
245249
width=8,
246250
key="change_percent",
247251
cell_factory=lambda q: PercentCell(
@@ -255,6 +259,7 @@ def _get_style_for_value(value: float) -> str:
255259
"open": (
256260
quote_column(
257261
"Open",
262+
full_name="Market Open",
258263
width=10,
259264
key="open",
260265
cell_factory=lambda q: FloatCell(
@@ -268,6 +273,7 @@ def _get_style_for_value(value: float) -> str:
268273
"low": (
269274
quote_column(
270275
"Low",
276+
full_name="Day Low",
271277
width=10,
272278
key="low",
273279
cell_factory=lambda q: FloatCell(
@@ -281,6 +287,7 @@ def _get_style_for_value(value: float) -> str:
281287
"high": (
282288
quote_column(
283289
"High",
290+
full_name="Day High",
284291
width=10,
285292
key="high",
286293
cell_factory=lambda q: FloatCell(
@@ -294,6 +301,7 @@ def _get_style_for_value(value: float) -> str:
294301
"_52w_low": (
295302
quote_column(
296303
"52w Low",
304+
full_name="52-Week Low",
297305
width=10,
298306
key="_52w_low",
299307
cell_factory=lambda q: FloatCell(
@@ -307,6 +315,7 @@ def _get_style_for_value(value: float) -> str:
307315
"_52w_high": (
308316
quote_column(
309317
"52w High",
318+
full_name="52-Week High",
310319
width=10,
311320
key="_52w_high",
312321
cell_factory=lambda q: FloatCell(
@@ -320,6 +329,7 @@ def _get_style_for_value(value: float) -> str:
320329
"volume": (
321330
quote_column(
322331
"Volume",
332+
full_name="Market Volume",
323333
width=10,
324334
key="volume",
325335
cell_factory=lambda q: CompactNumberCell(
@@ -332,6 +342,7 @@ def _get_style_for_value(value: float) -> str:
332342
"avg_volume": (
333343
quote_column(
334344
"Avg Vol",
345+
full_name="Average Daily Volume (3 Month)",
335346
width=10,
336347
key="avg_volume",
337348
cell_factory=lambda q: CompactNumberCell(
@@ -344,6 +355,7 @@ def _get_style_for_value(value: float) -> str:
344355
"pe": (
345356
quote_column(
346357
"P/E",
358+
full_name="Trailing Price-to-Earnings Ratio",
347359
width=6,
348360
key="pe",
349361
cell_factory=lambda q: FloatCell(
@@ -356,6 +368,7 @@ def _get_style_for_value(value: float) -> str:
356368
"dividend": (
357369
quote_column(
358370
"Div",
371+
full_name="Dividend Yield",
359372
width=6,
360373
key="dividend",
361374
cell_factory=lambda q: FloatCell(
@@ -368,6 +381,7 @@ def _get_style_for_value(value: float) -> str:
368381
"market_cap": (
369382
quote_column(
370383
"Mkt Cap",
384+
full_name="Market Capitalization",
371385
width=10,
372386
key="market_cap",
373387
cell_factory=lambda q: CompactNumberCell(

src/appui/quote_table.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ def quote_table() -> QuoteTable:
2929
return table
3030

3131

32-
def quote_column(
32+
def quote_column( # noqa: PLR0913
3333
label: str,
3434
*,
35+
full_name: str | None = None,
3536
key: str | None = None,
3637
width: int | None = None,
3738
justification: Justify | None = None,
@@ -41,6 +42,8 @@ def quote_column(
4142
4243
Args:
4344
label (str): The display label for the column.
45+
full_name (str | None): The full display name of the column. Defaults to
46+
the label when omitted.
4447
key (str | None): The key to access the attribute in YQuote.
4548
Defaults to None, which uses the label as the key.
4649
width (int | None): The width of the column.
@@ -55,12 +58,14 @@ def quote_column(
5558
class _QuoteColumnParams(TypedDict, total=False):
5659
"""Typing helper for optional QuoteColumn parameters."""
5760

61+
full_name: str
5862
key: str
5963
width: int
6064
justification: Justify
6165
cell_factory: Callable[[YQuote], EnhancedTableCell]
6266

6367
params: _QuoteColumnParams = {}
68+
params["full_name"] = full_name if full_name is not None else label
6469
if key is not None:
6570
params["key"] = key
6671
if width is not None:

tests/appui/test_column_chooser_screen.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
class _FakeColumn(ColumnMetadata):
2424
"""Minimal column metadata for tests."""
2525

26-
def __init__(self, key: str, label: str) -> None:
26+
def __init__(self, key: str, label: str, full_name: str | None = None) -> None:
2727
self._key = key
2828
self._label = label
29+
self._full_name = full_name or label
2930

3031
@property
3132
def key(self) -> str:
@@ -35,6 +36,10 @@ def key(self) -> str:
3536
def label(self) -> str:
3637
return self._label
3738

39+
@property
40+
def full_name(self) -> str:
41+
return self._full_name
42+
3843

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

151-
registry = _FakeRegistry([_FakeColumn("alpha", "Alpha Column")])
156+
registry = _FakeRegistry(
157+
[_FakeColumn("alpha", "Alpha Column", full_name="Alpha Column Full")]
158+
)
152159
container = _FakeContainer(active=[])
153160
app = _ColumnChooserTestApp(registry, container, DoubloonConfig())
154161

@@ -160,7 +167,9 @@ async def test_build_list_item_uses_registry_label() -> None:
160167
item = next(iter(screen._available_list.children))
161168
assert isinstance(item, ListItem)
162169
assert str(item.id) == "alpha"
163-
assert _label_text(item.query_one(Label)) == "Alpha Column"
170+
label = item.query_one(Label)
171+
assert _label_text(label) == "Alpha Column"
172+
assert label.tooltip == "Alpha Column Full"
164173

165174

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

170179
registry = _FakeRegistry(
171180
[
172-
_FakeColumn("frozen", "Frozen"),
181+
_FakeColumn("frozen", "Frozen", full_name="Frozen Column"),
173182
_FakeColumn("first", "First"),
174183
_FakeColumn("second", "Second"),
175184
_FakeColumn("third", "Third"),
@@ -184,7 +193,9 @@ async def test_populate_lists_excludes_frozen_and_preserves_order() -> None:
184193

185194
assert _list_item_ids(screen._available_list) == ["first", "third"]
186195
assert _list_item_ids(screen._active_list) == ["second"]
187-
assert [_label_text(label) for label in screen._frozen_labels] == ["Frozen"]
196+
frozen_label = screen._frozen_labels[0]
197+
assert _label_text(frozen_label) == "Frozen"
198+
assert frozen_label.tooltip == "Frozen Column"
188199

189200

190201
@pytest.mark.ui

tests/appui/test_enhanced_data_table.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def test_enhanced_column_defaults_apply(sample_row: SampleRow) -> None:
122122
column: EnhancedColumn[SampleRow] = EnhancedColumn(label="Name")
123123

124124
assert column.key == "Name"
125+
assert column.full_name == "Name"
125126
assert column.cell_factory is not None
126127
cell = column.cell_factory(sample_row)
127128
assert isinstance(cell, EnhancedTableCell)
@@ -199,9 +200,27 @@ def test_styled_column_label_places_arrows_correctly(
199200
assert label_text.startswith(prefix)
200201
assert label_text.endswith(suffix)
201202

202-
expected_core = column.label if not direction else column.label[: column.width - 2]
203-
core = label_text.removeprefix(prefix).removesuffix(suffix)
204-
assert core.strip() == expected_core.strip()
203+
204+
def test_header_hover_sets_tooltip_for_full_name() -> None:
205+
"""Hovering a header cell sets and clears the tooltip."""
206+
207+
table: EnhancedDataTable[SampleRow] = EnhancedDataTable()
208+
column = EnhancedColumn[SampleRow](
209+
label="Chg",
210+
key="change",
211+
full_name="Market Change",
212+
width=8,
213+
justification=Justify.RIGHT,
214+
)
215+
table.add_enhanced_column(column)
216+
217+
table.watch_hover_coordinate(Coordinate(0, 0), Coordinate(-1, 0))
218+
219+
assert table.tooltip == "Market Change"
220+
221+
table.watch_hover_coordinate(Coordinate(-1, 0), Coordinate(0, 0))
222+
223+
assert table.tooltip is None
205224

206225

207226
def test_get_styled_column_label_handles_sort_indicators(

tests/appui/test_quote_table.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __str__(self) -> str:
3030

3131
assert isinstance(column, EnhancedColumn)
3232
assert column.label == "Label"
33+
assert column.full_name == "Label"
3334
assert column.key == "Label"
3435
assert column.cell_factory is not None
3536
dummy_quote = cast("YQuote", DummyQuote())
@@ -49,6 +50,7 @@ def factory(q: YQuote) -> EnhancedTableCell:
4950

5051
column = quote_column(
5152
"Ticker",
53+
full_name="Ticker Symbol",
5254
width=test_width,
5355
key="ticker",
5456
justification=Justify.LEFT,
@@ -59,6 +61,7 @@ def factory(q: YQuote) -> EnhancedTableCell:
5961
assert column.key == "ticker"
6062
assert column.justification is Justify.LEFT
6163
assert column.cell_factory is factory
64+
assert column.full_name == "Ticker Symbol"
6265

6366

6467
def test_quote_table_factory_returns_enhanced_data_table() -> None:

0 commit comments

Comments
 (0)