Skip to content
54 changes: 53 additions & 1 deletion android/src/toga_android/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,68 @@ def change_source(self, source):
def after_on_refresh(self, widget, result):
self._refresh_layout.setRefreshing(False)

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's annoying because the overwhelming volume of changes in this PR are of this form... but is this style of shim needed? Is there a use case for an Android DetailedList implementation connecting to an old data source?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use-case would be if someone implemented their own list source rather than inheriting from it, or inherited and overrode notify. Not sure why they would do that but there could be a number of reasons (instrumentation, testing and different dispatch mechanisms come to mind).

But also, if the next release is a 0.6.0 rather than a 0.5.x, it's probably an OK thing to break, and it's easy enough to remove them: they were added by a big regex, they can be taken away by a big regex too!

def insert(self, index, item):
import warnings

warnings.warn(
"The insert() method is deprecated. Use source_insert() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_insert(index=index, item=item)

def source_insert(self, *, index, item):
self._load_data()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def change(self, item):
import warnings

warnings.warn(
"The change() method is deprecated. Use source_change() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_change(item=item)

def source_change(self, *, item):
self._load_data()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def remove(self, index, item):
import warnings

warnings.warn(
"The remove() method is deprecated. Use source_remove() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_remove(index=index, item=item)

def source_remove(self, *, index, item):
self._load_data()

def clear(self):
# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def clear(self, item):
import warnings

warnings.warn(
"The clear() method is deprecated. Use source_clear() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_clear()

def source_clear(self):
self._load_data()

def _clear_selection(self):
Expand Down
41 changes: 40 additions & 1 deletion android/src/toga_android/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ def on_change(self, index):
self.interface.on_change()
self.last_selection = index

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def insert(self, index, item):
import warnings

warnings.warn(
"The insert() method is deprecated. Use source_insert() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_insert(index=index, item=item)

def source_insert(self, *, index, item):
self.adapter.insert(self.interface._title_for_item(item), index)
if self.last_selection is None:
self.select_item(0)
Expand All @@ -51,7 +64,20 @@ def insert(self, index, item):
self.last_selection += 1
self.select_item(self.last_selection)

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def change(self, item):
import warnings

warnings.warn(
"The change() method is deprecated. Use source_change() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_change(item=item)

def source_change(self, *, item):
# Instead of calling self.insert and self.remove, use direct native calls to
# avoid disturbing the selection.
index = self.interface._items.index(item)
Expand Down Expand Up @@ -82,7 +108,20 @@ def get_selected_index(self):
selected = self.native.getSelectedItemPosition()
return None if selected == Spinner.INVALID_POSITION else selected

def clear(self):
# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def clear(self, item):
import warnings

warnings.warn(
"The clear() method is deprecated. Use source_clear() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_clear()

def source_clear(self):
self.adapter.clear()
self.on_change(None)

Expand Down
52 changes: 52 additions & 0 deletions android/src/toga_android/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,68 @@ def get_selection(self):
else:
return selection[0]

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def insert(self, index, item):
import warnings

warnings.warn(
"The insert() method is deprecated. Use source_insert() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_insert(index=index, item=item)

def source_insert(self, *, index, item):
self.change_source(getattr(self.interface, "data", None))

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def clear(self):
import warnings

warnings.warn(
"The clear() method is deprecated. Use source_clear() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_clear()

def source_clear(self):
self.change_source(getattr(self.interface, "data", None))

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def change(self, item):
import warnings

warnings.warn(
"The change() method is deprecated. Use source_change() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_change(item=item)

def source_change(self, *, item):
self.change_source(getattr(self.interface, "data", None))

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def remove(self, index, item):
import warnings

warnings.warn(
"The remove() method is deprecated. Use source_remove() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_remove(index=index, item=item)

def source_remove(self, *, index, item):
self.change_source(getattr(self.interface, "data", None))

def scroll_to_row(self, index):
Expand Down
1 change: 1 addition & 0 deletions changes/4046.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The list and tree listener protocols no longer incompatibly override the 'insert', 'remove' and 'clear' methods of list and tree sources, permitting mutable sources which are also listeners.
Comment thread
corranwebster marked this conversation as resolved.
Outdated
1 change: 1 addition & 0 deletions changes/4046.removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sources now look for methods with names of the form `source_{notification}`, rather than just `notification` when the `notify` method is called.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should include a migration note - something to the effect of "if you have defined a custom listener on a source, you should rename the methods on the Listner interface (such as insert, remove, and change) to have a source_ prefix.

52 changes: 52 additions & 0 deletions cocoa/src/toga_cocoa/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,72 @@ def create(self):
def change_source(self, source):
self.native_detailedlist.reloadData()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def insert(self, index, item):
import warnings

warnings.warn(
"The insert() method is deprecated. Use source_insert() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_insert(index=index, item=item)

def source_insert(self, *, index, item):
self.native_detailedlist.reloadData()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def change(self, item):
import warnings

warnings.warn(
"The change() method is deprecated. Use source_change() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_change(item=item)

def source_change(self, *, item):
self.native_detailedlist.reloadData()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def remove(self, index, item):
import warnings

warnings.warn(
"The remove() method is deprecated. Use source_remove() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_remove(index=index, item=item)

def source_remove(self, *, index, item):
self.native_detailedlist.reloadData()

# After deletion, the selection changes, but Cocoa doesn't send
# a tableViewSelectionDidChange: message.
self.interface.on_select()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def clear(self):
import warnings

warnings.warn(
"The clear() method is deprecated. Use source_clear() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_clear()

def source_clear(self):
self.native_detailedlist.reloadData()

def set_refresh_enabled(self, enabled):
Expand Down
52 changes: 52 additions & 0 deletions cocoa/src/toga_cocoa/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ def set_color(self, color):
def set_background_color(self, color):
pass

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def insert(self, index, item):
import warnings

warnings.warn(
"The insert() method is deprecated. Use source_insert() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_insert(index=index, item=item)

def source_insert(self, *, index, item):
# Issue 2319 - if item titles are not unique, macOS will move the existing item,
# rather than creating a duplicate item. To work around this, create an item
# with a temporary but unique name, then change the name. `_title_for_item()`
Expand All @@ -59,22 +72,61 @@ def insert(self, index, item):
if len(self.interface.items) == 1:
self.interface.on_change()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def change(self, item):
import warnings

warnings.warn(
"The change() method is deprecated. Use source_change() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_change(item=item)

def source_change(self, *, item):
index = self.interface._items.index(item)
native_item = self.native.itemAtIndex(index)
native_item.title = self.interface._title_for_item(item)
# Changing the item text can change the layout size
self.interface.refresh()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def remove(self, index, item):
import warnings

warnings.warn(
"The remove() method is deprecated. Use source_remove() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_remove(index=index, item=item)

def source_remove(self, *, index, item):
selection_change = self.native.indexOfSelectedItem == index

self.native.removeItemAtIndex(index)

if selection_change:
self.interface.on_change()

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def clear(self):
import warnings

warnings.warn(
"The clear() method is deprecated. Use source_clear() instead.",
DeprecationWarning,
stacklevel=1,
)
self.source_clear()

def source_clear(self):
self.native.removeAllItems()
self.interface.on_change()

Expand Down
Loading
Loading