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.Widget #1834

Merged
merged 27 commits into from
Apr 11, 2023
Merged

[widget audit] toga.Widget #1834

merged 27 commits into from
Apr 11, 2023

Conversation

freakboy3742
Copy link
Member

@freakboy3742 freakboy3742 commented Mar 30, 2023

An audit of the base widget.

Allows a widget to be assigned to a different app (should never happen in practice, but it makes testing easier)

Simplifies logic needing to verify if a widget has an implementation before it is added as a child. It's effectively impossible to create a widget that doesn't have an _impl - or, at least, the one case where it can happen (creating a box with children in the constructor) is trivial to avoid by adding the children after creating the impl.

Corrects a bug in Pack that would ignore an explicit width or height on a widget if the widget had children.

Corrects the handling of visibility, removing visibility = NONE as a style option (following CSS).

Adds a bare stub of a probe for TextInput. This is needed because we need a widget that can accept input focus. On Mobile, "input focus" means "show the keyboard", so we need a widget with keyboard input.

Includes a fix to remove the usage of the deprecated set_wmclass API on GTK.

The GTK implementation works, but I've had to mark the has_focus probe as a skip for now, because it appears that focus isn't being reported correctly in CI. I'm guessing this is because of CI.

Coverage on Winforms isn't strictly at 100% - however, of the 4 lines that aren't covered:

  • 2 are related to focus index handling. Focus index is only implemented on Winforms at this point, and there was some discussion about whether the API that was adopted was the best option, so it's untested generally. I've done an end-run around this problem by marking the feature as beta on the core API.
  • 1 is a branch that can't be exercised without having a toolbar in the test GUI
  • 1 is a branch that really shouldn't exist; Winforms needs a refactor analogous to the GTK container change to avoid issues with re-assigning the base container.

Fixes #1718

Audit checklist

  • Core tests ported to pytest
  • Core tests at 100% coverage
  • Core docstrings complete, and in Sphinx format
  • Documentation complete and consistently formatted
  • cocoa backend at 100% coverage
  • winforms backend at 100% coverage
  • gtk backend at 100% coverage
  • iOS backend at 100% coverage
  • android backend at 100% coverage
  • Widget support table updated

@freakboy3742 freakboy3742 force-pushed the audit-base branch 6 times, most recently from f6f72cd to d47445d Compare April 3, 2023 04:53
@freakboy3742 freakboy3742 marked this pull request as ready for review April 4, 2023 06:51
if not view.getClass().isInstance(self.native):
# save guard for Widgets like Canvas that are not based on View
self.interface.factory.not_implemented("Widget.set_hidden()")
return
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure why this was done as an isinstance check, rather than by overriding in the subclass, but I've moved it.

# but it's the only way I've found that actually sets the
# Application name to something other than '__main__.py'.
self.native.set_wmclass(app.interface.name, app.interface.name)

Copy link
Member Author

Choose a reason for hiding this comment

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

This keeps getting reported as a bug, even though it's clearly labeled a warning; since it's functionally broken on Ubuntu 22.04 (although it still works on Fedora 36) anyway, it seems better to remove the deprecated call and find a different approach.

@@ -15,7 +15,7 @@ class DatePicker(Widget):
a new one will be created for the widget.
"""

MIN_WIDTH = 200
Copy link
Member Author

Choose a reason for hiding this comment

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

This should probably be handled with a rehint based on the preferred size of the widget, rather than a hard coded value - but we can tackle that when we audit DatePicker.

@mhsmith
Copy link
Member

mhsmith commented Apr 9, 2023

I've reverted my last push and created #1853 instead.

docs/reference/api/widgets/button.rst Outdated Show resolved Hide resolved
core/tests/test_app.py Show resolved Hide resolved
docs/reference/api/widgets/switch.rst Outdated Show resolved Hide resolved
core/src/toga/widgets/base.py Outdated Show resolved Hide resolved
core/src/toga/widgets/base.py Outdated Show resolved Hide resolved
Comment on lines 28 to 31
def __del__(self):
if self.width_constraint:
self.width_constraint.release()
if self.height_constraint:
Copy link
Member

@mhsmith mhsmith Apr 9, 2023

Choose a reason for hiding this comment

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

This method isn't completely covered by the testbed. And if I understand correctly, a widget can only be associated with one Constraints object during its lifetime, so I'm not sure why it's necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're correct that a widget can only be associated with a single constraints object - but if a widget is deleted, we need to free the associated constraints object, which in turn means releasing the ObjC instances that have been explicitly retained.

The (missing) retain() calls weren't causing a problem on macOS, but they were on iOS; however, that's probably more a case of accidentally not hitting the problem, rather than the problem not being there.

Exercising this could be interesting... I'll see what I can do.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah - I can't work out a way to reliably get coverage here. The garbage collector of the ObjC runtime and the Python garbage collector both need to agree that a widget has been destroyed before __del__ is invoked on an interface widget; and that then needs to cascade down to the impl, and then the constraints object.

I've added a nocover to the two affected __del__ methods for now.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, isn't there a possible leak if a widget is removed from its container and then added to another container? When it's added to the new container, new constraints objects are created without releasing the old ones.

The simplest solution would be to allocate the constraints objects in the constructor and then reuse them for all containers in the widget's lifetime. Or if that isn't possible because they're tied to the container, maybe the content of __del__ could be factored out into another method, and called from the if value is None and self.container branch of container.setter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch on the leak when reparenting; I'll fix that case.

Agreed that creating the constraint objects once would be preferable - except that the container is an argument to the top and left constraints. We'd also need to manage adding and removing the constraint from the existing containers. At that point, the accounting associated with destroying and creating fresh constraints isn't that far removed from what we're already doing.

Refactoring the cleanup code into a common location is definitely a good idea though, and it cleans up the setter as well, so I've taken that approach.

# print("Add constraints for", self.widget, 'in', self.container, self.widget.interface.layout)
self.left_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # NOQA:E501
# print(f"Add constraints for {self.widget} in {self.container} {self.widget.interface.layout})
self.left_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
Copy link
Member

Choose a reason for hiding this comment

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

Could these E501 errors be fixed by using keyword argument syntax instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately not. See beeware/rubicon-objc#148.

cocoa/src/toga_cocoa/widgets/base.py Show resolved Hide resolved
iOS/src/toga_iOS/constraints.py Outdated Show resolved Hide resolved
iOS/src/toga_iOS/constraints.py Show resolved Hide resolved
@mhsmith mhsmith merged commit acc5cbe into beeware:main Apr 11, 2023
@mhsmith
Copy link
Member

mhsmith commented Apr 11, 2023

Sorry, looks like I merged some end of line whitespace. I've pushed a fix to #1861.

@freakboy3742 freakboy3742 deleted the audit-base branch April 12, 2023 03:28
@freakboy3742 freakboy3742 mentioned this pull request Apr 15, 2023
15 tasks
@freakboy3742 freakboy3742 mentioned this pull request May 15, 2023
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Warnings when running Hello World script
2 participants