Skip to content
52 changes: 52 additions & 0 deletions 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()

# 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._load_data()

def _clear_selection(self):
Expand Down
52 changes: 52 additions & 0 deletions 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,14 +64,40 @@ 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)
self.adapter.insert(self.interface._title_for_item(item), index)
self.adapter.remove(self.adapter.getItem(index + 1))

# Alias for backwards compatibility:
# March 2026: In 0.5.3 and earlier, notification methods
# didn't start with 'source_'
def remove(self, index, item=None):
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=None):
self.adapter.remove(self.adapter.getItem(index))

# Adjust the selection index, but only generate an event if the selected item
Expand All @@ -82,7 +121,20 @@ def get_selected_index(self):
selected = self.native.getSelectedItemPosition()
return None if selected == Spinner.INVALID_POSITION else selected

# 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.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 `ListListener` and `TreeListener` protocols no longer incompatibly override the `insert`, `remove` and `clear` methods of `ListSource` and `TreeSource`, permitting mutable sources which are also listeners.
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. If you have a custom listener class with methods like `change`, `insert`, `remove` or `clear`, you should re-name them to have a `source_` prefix: `source_change`, `source_insert` and so on.
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