Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[widget Audit] toga.DetailedList #2025

Merged
merged 33 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8a3fbf1
Update docs and core API for DetailedList.
freakboy3742 Jul 4, 2023
796dbb2
Add changenotes.
freakboy3742 Jul 4, 2023
9a4e69f
Update docs index entry.
freakboy3742 Jul 4, 2023
e6e20c7
Update core tests for DetailedList.
freakboy3742 Jul 4, 2023
186aff3
Update examples to use new ListSource APIs.
freakboy3742 Jul 4, 2023
545a280
Deleting attributes is a notifiable change.
freakboy3742 Jul 4, 2023
1f260f5
Modify core API to use primary/secondary actions.
freakboy3742 Jul 5, 2023
6f49004
Cocoa DetailedList at 100%.
freakboy3742 Jul 6, 2023
6ec8eb8
iOS DetailedList to 100% coverage.
freakboy3742 Jul 6, 2023
ced26bb
Silence a test cleanup warning when running iOS tests non-slow.
freakboy3742 Jul 7, 2023
6484af2
More iOS test cleanups.
freakboy3742 Jul 7, 2023
b7fa33d
Clean up GTK DetailedList implementation
freakboy3742 Jul 7, 2023
dea9eaf
GTK DetailedList at 100% coverage.
freakboy3742 Jul 8, 2023
50b07dd
Add the ability to get a coverage report without running the full tes…
freakboy3742 Jul 8, 2023
6597a16
Ensure widgets have been made visible; Fixes #2026.
freakboy3742 Jul 8, 2023
cd02bfd
Merge branch 'audit-tree' into audit-detailedlist
freakboy3742 Jul 8, 2023
b3ac24a
Ensure GTK progressbar gets coverage.
freakboy3742 Jul 9, 2023
3dec76a
Use explicit calls to tableView methods to avoid ambiguous names, and…
freakboy3742 Jul 9, 2023
f92d9f8
Merge branch 'audit-tree' into audit-detailedlist
freakboy3742 Jul 17, 2023
230d3bb
Merge branch 'audit-tree' into audit-detailedlist
freakboy3742 Jul 26, 2023
0fe227b
Merge branch 'audit-tree' into audit-detailedlist
freakboy3742 Jul 26, 2023
12dc04e
Merge branch 'audit-tree' into audit-detailedlist
freakboy3742 Aug 4, 2023
812fe80
Merge branch 'main' into audit-detailedlist
freakboy3742 Aug 29, 2023
52be479
Documentation cleanups
mhsmith Aug 30, 2023
9d0a282
Winforms: support icons in Tables, but in first column only
mhsmith Sep 1, 2023
449a17a
WinForms DetailedList at 100%, but primary, secondary and refresh act…
mhsmith Sep 1, 2023
1dd7fea
In examples/table, make data structure clearer
mhsmith Sep 4, 2023
4d26f01
Remove workarounds for rubicon-java JNI reference issues
mhsmith Sep 6, 2023
b1b57d2
Android DetailedList at 100%
mhsmith Sep 7, 2023
ba31939
Make simulated Android swipes more realistic
mhsmith Sep 9, 2023
2b4ff7a
Rationalize usage of MainActivity.singletonThis
mhsmith Sep 9, 2023
09cb2a5
Move nested classes to module level
mhsmith Sep 9, 2023
4ff988c
Clarify DetailedList accessor docs
mhsmith Sep 9, 2023
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
4 changes: 4 additions & 0 deletions android/tests_backend/widgets/progressbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ def is_animating_indeterminate(self):
@property
def position(self):
return self.native.getProgress() / self.native.getMax()

async def wait_for_animation(self):
# Android ProgressBar has internal animation handling; no special handling required.
pass
1 change: 1 addition & 0 deletions changes/2025.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The DetailedList widget now has 100% test coverage and complete API documentation.
1 change: 1 addition & 0 deletions changes/2025.feature.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The accessors used to populate a DetailedList can now be customised.
1 change: 1 addition & 0 deletions changes/2025.feature.3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A DetailedList can now provide a value to use when a row doesn't provide the required data.
1 change: 1 addition & 0 deletions changes/2025.feature.4.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DetailedList can now respond to "primary" and "secondary" user actions. These may be implemented as left and right swipe respectively, or using any other platform-appropriate mechanism.
1 change: 1 addition & 0 deletions changes/2025.removal.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When constructing a DetailedList from a list of tuples, or a list of lists, the required order of values has changed from (icon, title, subtitle) to (title, subtitle, icon).
1 change: 1 addition & 0 deletions changes/2025.removal.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``on_select`` handler for DetailedList no longer receives the selected row as an argument.
1 change: 1 addition & 0 deletions changes/2025.removal.3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The handling of row deletion in DetailedList widgets has been significantly altered. The ``on_delete`` event handler has been renamed ``on_primary_action``, and is now *only* a notification that a "swipe left" event (or platform equivalent) has been confirmed. This was previously inconsistent across platforms. Some platforms would update the data source to remove the row; some treated ``on_delete`` as a notification event and expected the application to handle the deletion. It is now the application's responsibility to perform the data deletion.
174 changes: 100 additions & 74 deletions cocoa/src/toga_cocoa/widgets/detailedlist.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,67 @@
from rubicon.objc import SEL, objc_method, objc_property
from travertino.size import at_least

from toga_cocoa.libs import (
SEL,
NSBezelBorder,
NSIndexSet,
NSMenu,
NSTableColumn,
NSTableView,
NSTableViewColumnAutoresizingStyle,
objc_method,
objc_property,
)
from toga_cocoa.widgets.base import Widget
from toga_cocoa.widgets.internal.cells import TogaDetailedCell
from toga_cocoa.widgets.internal.data import TogaData
from toga_cocoa.widgets.internal.refresh import RefreshableScrollView


def attr_impl(value, attr):
# If the data value has an _impl attribute, invoke it.
# This will manifest any impl-specific attributes.
impl = getattr(value, attr, None)
try:
return impl._impl
except AttributeError:
return impl


class TogaList(NSTableView):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)

@objc_method
def menuForEvent_(self, event):
if self.interface.on_delete:
if self.impl.primary_action_enabled or self.impl.secondary_action_enabled:
# Find the row under the mouse click
mousePoint = self.convertPoint(event.locationInWindow, fromView=None)
row = self.rowAtPoint(mousePoint)

popup = NSMenu.alloc().initWithTitle("popup")
delete_item = popup.addItemWithTitle(
"Delete", action=SEL("actionDeleteRow:"), keyEquivalent=""
# Ensure the row is selected.
self.selectRowIndexes(
NSIndexSet.indexSetWithIndex(row),
byExtendingSelection=False,
)
delete_item.tag = row
# action_item = popup.addItemWithTitle("???", action=SEL('actionRow:'), keyEquivalent="")
# action_item.tag = row

# Create a popup menu to display the possible actions.
popup = NSMenu.alloc().initWithTitle("popup").autorelease()
if self.impl.primary_action_enabled:
primary_action_item = popup.addItemWithTitle(
self.interface._primary_action,
action=SEL("primaryActionOnRow:"),
keyEquivalent="",
)
primary_action_item.tag = row

if self.impl.secondary_action_enabled:
secondary_action_item = popup.addItemWithTitle(
self.interface._secondary_action,
action=SEL("secondaryActionOnRow:"),
keyEquivalent="",
)
secondary_action_item.tag = row

return popup
else:
return None

@objc_method
def actionDeleteRow_(self, menuitem):
def primaryActionOnRow_(self, menuitem):
row = self.interface.data[menuitem.tag]
self.interface.on_delete(self.interface, row=row)
self.interface.on_primary_action(self.interface, row=row)

@objc_method
def secondaryActionOnRow_(self, menuitem):
row = self.interface.data[menuitem.tag]
self.interface.on_secondary_action(self.interface, row=row)

# TableDataSource methods
@objc_method
Expand All @@ -66,7 +78,34 @@ def tableView_objectValueForTableColumn_row_(self, table, column, row: int):
data.retain()
value._impl = data

data.attrs = {attr: attr_impl(value, attr) for attr in value._attrs}
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

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

data.attrs = {
"title": title,
"subtitle": subtitle,
"icon": icon,
}

return data

Expand All @@ -83,103 +122,90 @@ def selectionShouldChangeInTableView_(self, table) -> bool:

@objc_method
def tableViewSelectionDidChange_(self, notification) -> None:
index = notification.object.selectedRow
if index == -1:
selection = None
else:
selection = self.interface.data[index]

if self.interface.on_select:
self.interface.on_select(self.interface, row=selection)
self.interface.on_select(self.interface)


class DetailedList(Widget):
def create(self):
# Create a List, and put it in a scroll view.
# The scroll view is the _impl, because it's the outer container.
self.native = RefreshableScrollView.alloc().init()
self.native.interface = self.interface
self.native.impl = self
self.native.hasVerticalScroller = True
self.native.hasHorizontalScroller = False
self.native.autohidesScrollers = False
self.native.borderType = NSBezelBorder

# Create the List widget
self.detailedlist = TogaList.alloc().init()
self.detailedlist.interface = self.interface
self.detailedlist.impl = self
self.detailedlist.columnAutoresizingStyle = (
self.native_detailedlist = TogaList.alloc().init()
self.native_detailedlist.interface = self.interface
self.native_detailedlist.impl = self
self.native_detailedlist.columnAutoresizingStyle = (
NSTableViewColumnAutoresizingStyle.Uniform
)
self.native_detailedlist.allowsMultipleSelection = False

# TODO: Optionally enable multiple selection
self.detailedlist.allowsMultipleSelection = False
# Disable all actions by default.
self.primary_action_enabled = False
self.secondary_action_enabled = False

self.native.detailedlist = self.detailedlist
self.native = RefreshableScrollView.alloc().initWithDocument(
self.native_detailedlist
)
self.native.interface = self.interface
self.native.impl = self

# Create the column for the detailed list
column = NSTableColumn.alloc().initWithIdentifier("data")
self.detailedlist.addTableColumn(column)
self.native_detailedlist.addTableColumn(column)
self.columns = [column]

cell = TogaDetailedCell.alloc().init()
column.dataCell = cell

# Hide the column header.
self.detailedlist.headerView = None
self.native_detailedlist.headerView = None

self.detailedlist.delegate = self.detailedlist
self.detailedlist.dataSource = self.detailedlist

# Embed the tree view in the scroll view
self.native.documentView = self.detailedlist
self.native_detailedlist.delegate = self.native_detailedlist
self.native_detailedlist.dataSource = self.native_detailedlist

# Add the layout constraints
self.add_constraints()

def change_source(self, source):
self.detailedlist.reloadData()
self.native_detailedlist.reloadData()

def insert(self, index, item):
self.detailedlist.reloadData()
self.native_detailedlist.reloadData()

def change(self, item):
self.detailedlist.reloadData()
self.native_detailedlist.reloadData()

def remove(self, index, item):
self.detailedlist.reloadData()
self.native_detailedlist.reloadData()

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

def clear(self):
self.detailedlist.reloadData()
self.native_detailedlist.reloadData()

def set_refresh_enabled(self, enabled):
self.native.setRefreshEnabled(enabled)

def set_on_refresh(self, handler):
pass
def set_primary_action_enabled(self, enabled):
self.primary_action_enabled = enabled

def set_secondary_action_enabled(self, enabled):
self.secondary_action_enabled = enabled

def after_on_refresh(self, widget, result):
self.native.finishedLoading()

def get_selection(self):
index = self.detailedlist.selectedRow
if index != -1:
return self.interface.data[index]
else:
index = self.native_detailedlist.selectedRow
if index == -1:
return None

def set_on_select(self, handler):
pass

def set_on_delete(self, handler):
pass
else:
return index

def scroll_to_row(self, row):
self.detailedlist.scrollRowToVisible(row)
self.native_detailedlist.scrollRowToVisible(row)

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
Expand Down
Loading
Loading