Skip to content
Open
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
20 changes: 6 additions & 14 deletions android/src/toga_android/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,10 @@ def _make_row(self, container, i, row):
row_view.setOnLongClickListener(DetailedListOnLongClickListener(self, i))
row_height = self.scale_in(64)

title, subtitle, icon = (
getattr(row, attr, None) for attr in self.interface.accessors
)

# Add user-provided icon to layout.
icon_image_view = ImageView(self._native_activity)
if icon is not None:
icon_image_view.setImageBitmap(icon._impl.native)
if self.interface._icon(row) is not None:
icon_image_view.setImageBitmap(self.interface._icon(row)._impl.native)
icon_layout_params = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT,
Expand All @@ -172,18 +168,14 @@ def _make_row(self, container, i, row):
text_container.setOrientation(LinearLayout.VERTICAL)
text_container.setWeightSum(2.0)

# Create top & bottom text; add them to layout.
def get_string(value):
if value is None:
value = self.interface.missing_value
return str(value)

# _title() and _subtitle() handle None, convert to str, and return only the
# first line so that newline characters are not rendered by the TextView.
top_text = TextView(self._native_activity)
top_text.setText(get_string(title))
top_text.setText(self.interface._title(row))
top_text.setTextSize(20.0)
top_text.setTextColor(self.get_theme_color(R.attr.textColorPrimary))
bottom_text = TextView(self._native_activity)
bottom_text.setText(get_string(subtitle))
bottom_text.setText(self.interface._subtitle(row))
bottom_text.setTextSize(16.0)
bottom_text.setTextColor(self.get_theme_color(R.attr.textColorSecondary))
top_text_params = LinearLayout.LayoutParams(
Expand Down
1 change: 1 addition & 0 deletions changes/2343.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `DetailedList` widget now displays only the first line of title and subtitle text.
25 changes: 4 additions & 21 deletions cocoa/src/toga_cocoa/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,11 @@ def tableView_objectValueForTableColumn_row_(self, table, column, row: int):
data = TogaData.alloc().init()
value._impl = data

try:
title = getattr(value, self.interface.accessors[0])
if title is not None:
title = str(title)
else:
title = self.interface.missing_value
except AttributeError:
title = self.interface.missing_value
title = self.interface._title(value)
subtitle = self.interface._subtitle(value)

try:
subtitle = getattr(value, self.interface.accessors[1])
if subtitle is not None:
subtitle = str(subtitle)
else:
subtitle = self.interface.missing_value
except AttributeError:
subtitle = self.interface.missing_value

try:
icon = getattr(value, self.interface.accessors[2])._impl.native
except AttributeError:
icon = None
icon_obj = self.interface._icon(value)
icon = None if icon_obj is None else icon_obj._impl.native

data.attrs = {
"title": title,
Expand Down
38 changes: 38 additions & 0 deletions core/src/toga/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,44 @@ def missing_value(self) -> str:
"""
return self._missing_value

def _as_text(self, value: object) -> str:
"""Convert a raw value to a single-line display string (internal helper).

If the value is None, the widget's missing_value is used instead.
Only the first line of the string representation is returned, so
that titles and subtitles containing newline characters are not
rendered as multiple lines.
"""
if value is None:
value = self.missing_value
text = str(value)
lines = text.splitlines()
return lines[0] if lines else ""

def _title(self, row: object) -> str:
"""Retrieve the title for a row as a single-line display string.

Reads the title attribute (first accessor), substitutes missing_value
for None, and truncates at the first newline character.
"""
return self._as_text(getattr(row, self._accessors[0], None))

def _subtitle(self, row: object) -> str:
"""Retrieve the subtitle for a row as a single-line display string.

Reads the subtitle attribute (second accessor), substitutes missing_value
for None, and truncates at the first newline character.
"""
return self._as_text(getattr(row, self._accessors[1], None))

def _icon(self, row: object) -> object:
"""Retrieve the icon for a row.

Returns the icon object (third accessor), or None if the attribute
is absent or not set.
"""
return getattr(row, self._accessors[2], None)

@property
def selection(self) -> Row | None:
"""The current selection of the table.
Expand Down
64 changes: 64 additions & 0 deletions core/tests/widgets/test_detailedlist.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from types import SimpleNamespace
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -321,3 +322,66 @@ def test_scroll_to_bottom(detailedlist):
detailedlist.scroll_to_bottom()

assert_action_performed_with(detailedlist, "scroll to row", row=2)


@pytest.mark.parametrize(
"value, expected",
[
# Plain text is returned unchanged
("hello", "hello"),
# Non-string values are converted to string first
(42, "42"),
# None is replaced by the widget's missing_value (default is "")
(None, ""),
# Only the first line is returned when \n is present
("hello\nworld", "hello"),
# Windows-style line endings (\r\n) are also handled
("hello\r\nworld", "hello"),
# A leading newline means the first line is empty
("\nhello", ""),
# A completely empty string stays empty
("", ""),
],
)
def test_as_text(detailedlist, value, expected):
"""_as_text() converts a value to a single-line display string.

Only the first line of the string representation is returned.
None values are replaced by the widget's missing_value.
"""
assert detailedlist._as_text(value) == expected


def test_title(detailedlist):
"""_title() reads the title accessor from a row and returns a single-line string."""
# Multiline value is truncated at the first newline
row = SimpleNamespace(key="hello\nworld", value="subtitle", icon=None)
assert detailedlist._title(row) == "hello"


def test_title_missing_attribute(detailedlist):
"""_title() returns missing_value when the row has no title attribute."""
row = SimpleNamespace(value="subtitle", icon=None) # no 'key' attribute
assert detailedlist._title(row) == "" # default missing_value is ""


def test_subtitle(detailedlist):
"""_subtitle() reads the subtitle attribute from a row as a single-line string."""
row = SimpleNamespace(key="title", value="hello\nworld", icon=None)
assert detailedlist._subtitle(row) == "hello"


def test_subtitle_missing_attribute(detailedlist):
"""_subtitle() returns missing_value when the row has no subtitle attribute."""
row = SimpleNamespace(key="title", icon=None) # no 'value' attribute
assert detailedlist._subtitle(row) == "" # default missing_value is ""


def test_icon(detailedlist):
"""_icon() returns the icon attribute of a row, or None if absent."""
mock_icon = object()
row = SimpleNamespace(key="", value="", icon=mock_icon)
assert detailedlist._icon(row) is mock_icon
assert detailedlist._icon(SimpleNamespace(key="", value="", icon=None)) is None
# Missing attribute returns None via getattr default
assert detailedlist._icon(SimpleNamespace(key="", value="")) is None
25 changes: 4 additions & 21 deletions gtk/src/toga_gtk/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,8 @@ def update(self, dl, row):
and accessors from the detailedList"""

# Set the title and subtitle as a block of HTML text.
try:
title = getattr(self.row, dl.accessors[0])
if title is not None:
title = str(title)
else:
title = dl.missing_value
except AttributeError:
title = dl.missing_value

try:
subtitle = getattr(self.row, dl.accessors[1])
if subtitle is not None:
subtitle = str(subtitle)
else:
subtitle = dl.missing_value
except AttributeError:
subtitle = dl.missing_value
title = dl._title(self.row)
subtitle = dl._subtitle(self.row)

markup = "".join(
[
Expand All @@ -89,10 +74,8 @@ def update(self, dl, row):
if self.icon:
self.content.remove(self.icon)

try:
pixbuf = getattr(self.row, dl.accessors[2])._impl.native(32)
except AttributeError:
pixbuf = None
icon = dl._icon(self.row)
pixbuf = icon._impl.native(32) if icon is not None else None

if pixbuf is not None:
self.icon = Gtk.Image.new_from_pixbuf(pixbuf)
Expand Down
29 changes: 5 additions & 24 deletions iOS/src/toga_iOS/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,11 @@ def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):

value = self.interface.data[indexPath.item]

try:
label = getattr(value, self.interface.accessors[0])
if label is None:
cell.textLabel.text = self.interface.missing_value
else:
cell.textLabel.text = str(label)
except AttributeError:
cell.textLabel.text = self.interface.missing_value

try:
label = getattr(value, self.interface.accessors[1])
if label is None:
cell.detailTextLabel.text = self.interface.missing_value
else:
cell.detailTextLabel.text = str(label)
except AttributeError:
cell.detailTextLabel.text = self.interface.missing_value

try:
cell.imageView.image = getattr(
value, self.interface.accessors[2]
)._impl.native
except AttributeError:
cell.imageView.image = None
cell.textLabel.text = self.interface._title(value)
cell.detailTextLabel.text = self.interface._subtitle(value)

icon = self.interface._icon(value)
cell.imageView.image = None if icon is None else icon._impl.native

return cell

Expand Down
11 changes: 9 additions & 2 deletions qt/src/toga_qt/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,15 @@ def create(self):
self.native_model = ListSourceModel(
self.interface.data,
{
Qt.ItemDataRole.UserRole: user_formatter,
Qt.ItemDataRole.DecorationRole: icon_formatter,
Qt.ItemDataRole.UserRole: lambda row, _: (
self.interface._title(row),
self.interface._subtitle(row),
),
Qt.ItemDataRole.DecorationRole: lambda row, _: (
icon._impl.native
if (icon := self.interface._icon(row)) is not None
else QIcon()
),
},
parent=self.native,
)
Expand Down
10 changes: 3 additions & 7 deletions winforms/src/toga_winforms/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,14 +746,10 @@ def _image_index(self, icon):

def _new_item(self, index) -> tuple[str, str, int]:
row = self._data[index]

title, subtitle, icon = (
getattr(row, attr, None) for attr in self.interface.accessors
)

icon = self.interface._icon(row)
return (
str(self._missing_value) if title is None else str(title),
str(self._missing_value) if subtitle is None else str(subtitle),
self.interface._title(row),
self.interface._subtitle(row),
-1 if icon is None else self._image_index(icon._impl),
)

Expand Down
Loading