-
-
Notifications
You must be signed in to change notification settings - Fork 668
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
Column renderers #1085
Column renderers #1085
Conversation
151386a
to
a53bc2a
Compare
Also, I have not yet changed any of the tests, etc. This PR is just to get some initial feedback about the API and to get an idea of what is required to adapt the backends. |
0402bad
to
33493e3
Compare
Ok, so I have adapted the winforms backend as well now (only supporting text renderers) and updated the tests. The @freakboy3742, could you already have a look at this and check if you like concept behind the changes? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the delay in reviewing this - I've had a lot going on in Real Life at the moment.
Broadly - I like what I see here. I've nitpicked a couple of small things that stood out; I'm also not 100% sure about the top-level organization of toga.renderers
- it feels to me like they should be a submodule of toga.widget
; but the bones of what you've laid out here look really good to me. I'm sure I'll find more when it gets time for a final review.
This is obviously backwards incompatible (changing headings
to columns
) - and while the new name makes sense, I think there's enough code in the wild using the old API that we should provide a simple backwards compatibility shim (since, AFAICT, it should be a fairly simple mapping in this case).
The only thing I'm not sure about is how this maps onto "inputs" in the data source. Is the plan to add Renderer classes to handle input elements? And if so - how do those input widgets tie back to the renderer, and then the data source?
examples/table/table/app.py
Outdated
# Data to populate the table. | ||
data = [] | ||
for x in range(5): | ||
data.append(tuple(str(x) for x in range(5))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example was deliberate; it's using a native Python list rather than a ListSource to highlight that you can use simple datatypes as a source, and they'll be converted automatically to a Data Source.
@@ -59,10 +60,13 @@ def startup(self): | |||
# Label to show responses. | |||
self.label = toga.Label('Ready.', style=Pack(padding=10)) | |||
|
|||
data_source = TreeSource([], accessors=["year", "title", "rating", "genre"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As with the table demo, the choice to not use an explicit Source here was deliberate.
No problem about the late review, changes across so many modules are a lot of work. Regarding the issues you raised: Module:I have thought about adding renderers as a submodule of Backwards compatibility:That should be relatively easy to implement. Input elements:I am planning to add it to the renderers but I'm still not sure about the best API. At the moment, the renderers already support "input widgets": text in a table cell has always been editable in toga(-cocoa) and I have already added support for rendering checkboxes in the cocoa and gtk backends. However, a way to propagate changes in the table view back to the data source is still missing. The issue with updating the source is as follows: at the moment, a renderer attribute can accept a number of column data types. For example, the
Any opinions? |
ea5a73f
to
7c160d0
Compare
After some delays, I've finally found some time to come back to this and decided to make a few changes.
TODOs:
|
cfde0d1
to
bcc4840
Compare
@freakboy3742, I know it's been a while, but could you have another look at this? The changes are outlined in the previous post. The major change has been the removal of the Also new is the capability of editing text and checkbox values and seeing those edits reflected in the data source. A good example to showcase this is I haven't implemented any width settings yet since I'd like your input on that. Ideally settings such as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broadly, I'm really happy with the direction this is going. I need to wrap my head around some of the internal changes to rendering - they clearly work, but I want to make sure I understand them fully.
From a high level, there are two big things that stand out for me:
The first is the the use of widget.Column
/toga.Column
as a public API. The implication is that Column
is something that can be used standalone, which clearly won't ever be true. I'm wondering if this might a time where an inner class might make sense, making the external API toga.Table.Column
and toga.Tree.Column
. Since Column is identical for Table and Tree, it might also make sense for there to be a single "internal" implementation of Column that is then namespace as toga.Table.Column
and toga.Tree.Column
.
The second is the public handler interface around columns. There's obviously a need to handle events at the level of Column to handle editability etc, but I'm not sure it follows that we need to have a public handler interface on Column to handle on_click/on_change events. In my mind, a Tree/Table is a visualization of a data source. A GUI change (such as editing text or checking a box) should be modifying the data source, and it should be the data source telling you that there's been a change.
This isn't a distinction that we currently make with "single datum" widgets (like TextInput) - but there's 2 reasons for that:
-
While ValueSource exists in the toga API, we're not currently providing support for it anywhere. We should be, though - that's been on my todo list for a long time
-
When a widget controls a single value, it's a lot easier to think of the widget as the value, especially when the value isn't connected to anything more abstract.
In the long term, it would be easy to think of a simplified API where a TextInput has a ValueInput by default, and setting an on_change handler on the widget is a proxy for the underlying data source; however, I'm not sure it makes sense for the same to be true for Tree/Table, as those widgets wrap a list of rows, not a single value.
Do those ideas make any sense to you?
I completely agree with you and was already thinking along the same lines.
I did not think of that but do agree with your logic here. It is indeed a bit awkward to have the callbacks attached to the column when it is really the data which has changed. This could be implemented for instance through the setters of the data source. My only concern is distinguishing between programmatic changes of the data vs user interface changes. At the moment, single value widgets in toga only call the handler when the value is changes through the GUI. When setting the handlers on the data store, it would make sense for them to be called also when the data changes programmatically. Qt for instance emits different signals for programmatic vs GUI changes to a value. |
046ed19
to
e45add9
Compare
image_view = NSImageView.alloc().init() | ||
text_field = NSTextField.alloc().init() | ||
def initWithLayout(self): | ||
send_super(__class__, self, "init") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handling init this way is cleaner but comes with a significant performance impact. Though I prefer having a clean implementation here and working on rubicon-objc to improve the performance.
Completely agreed. There's a definite risk of a "GUI change occurs, issues signal to data source, data source changes, issues signal to GUI..." cycle that we need to be careful to prevent.
That would be one option; another might be a flag on data-based signals that indicates whether the change was initiated by a data change or as a result of a UI change. Whatever approach we take, this particular detail needs a lot of attention to make sure we get the details right. |
Thinking about this, Regarding cycles: there already is a bit of a cycle at the moment: Modifying a row in the GUI will cause the "change" listener to be called, which will update all views that are attached to the data source, including the view that caused the change. However, this programmatic update of the GUI does not trigger anything else, breaking the cycle at this point. I was more concerned about programmatic vs GUI changes to the data source as a convenience to toga users. They might want or expect handlers to be only invoked due to GUI changes, as is the case throughout all of toga at the moment. |
I have improved the documentation for Of course it would be nicer in the long term to provide an API for data source changes which is more inline with toga's regular "on_change" handler API. However, I am reluctant to add two different APIs for handlers to the data source interface and I am equally reluctant to redesign the existing listeners / notifications API at the moment. |
3f39a75
to
2aec1e7
Compare
I've added code to set the column width from the column's @freakboy3742, this should be ready for a proper review now. I still need to improve test coverage but would ideally like to do that after making any requested changes. |
The width is only set once during init and not applied via toga's layout engine. This means that columns can be resized by the end user through the GUI later.
46c0b6c
to
9c94488
Compare
Codecov Report
|
@freakboy3742, I was looking at the column style again today and realised that I am happy with the current solution. Only the column width has any meaning and traditional layout mechanisms by What is more, the current style attributes What do you think? Would you be happy to leave things as is? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So - there's a lot of good stuff here; there's still some things that need to be addressed, though:
- Regarding your question about style: I agree that properties like
width
should only be applied at construction. That said, properties likeflex
do need to be kept in consideration during a resize (or, at least, they should be). The good news is that the layout inside the table is almost independent of the Pack algorithm; and as a first pass, if we didn't handle flex, I'd be OK with that. - This has obviously drifted from master; that's my fault for not responding to this sooner, so apologies for that. I took a quick pass at merging, but the resulting code threw segfaults in some internal cell rendering code on Cocoa; I'm not sure if that's because of my merge, or some other conflict that has been introduced.
- That said - part of the reason for the drift is that there's a lot of changes here that are really good, but completely unrelated to core of the change - things like docstring and type annotation updates. It may assist the merge process if this could be broken into smaller chunks - one PR to get the groundwork straight, and then one to update the widgets. The good news is that I can commit to much more rapid code reviews now, so it's a lot less likely that PRs will go stale due to lack of attention.
- My biggest concern at this point is backwards compatibility. I've flagged at least one attribute that is currently in the wild that will break the current API if it is used (
missing_value
); there's also a whole pattern of usage (providing a list of data that is transparently turned into a data source) that has been effectively deprecated by this change. There's also a lot of requirement for explicit attributes where they were once implied. On the one hand, there's the Zen of Python argument that explicit > implicit; but on the other hand, being able to just throw data at a table and have it render, or just throw some columns at a table and have it infer the obvious accessor was a nifty trick. If we're going to deprecate these uses, I'd like to make sure we're being explicit about it.
MIN_WIDTH = 100 | ||
MIN_HEIGHT = 100 | ||
|
||
def __init__(self, headings, id=None, style=None, data=None, accessors=None, | ||
multiple_select=False, on_select=None, on_double_click=None, | ||
missing_value=None, factory=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to provide a backwards compatibility path for the missing_value
argument.
On the subject of which... what is the replacement for this fallback behaviour?
I agree that it probably makes sense to split / redo this PR instead of trying to resolve merge conflicts. Your other concerns, especially regarding backward compatibility, are also valid, though the current API is a little bit too implicit for my taste... Regarding the segfaults, I suspect this may have something to do with merging #1328 and / or recent changes to memory management in rubicon-objc. The row views should be retained and reused on the Cocoa side, this is what maintains good performance even for a large number of rows. However, we may be releasing the views somewhere unintentionally. I might find some time to look at this is in more detail next week, though these days I am having trouble keeping up with your pace! |
Closing in favour of #1478. |
This PR introduces the concept of
Columns
fortoga.Table
andtoga.Tree
at the interface layer, as proposed in #1066. The columns define how the data is displayed through properties such asmin_width
,max_width
and arenderer
. Therenderer
entirely defines how a table column should be populated from the data source, taking values from one or more accessors in the data source and using them to display text, icons, checkboxes, etc.Basic implementations of some renderers for the Cocoa and Gtk backends are also provided. Any editing functionality is missing.
The resulting usage is as follows:
Columns can be provided in several forms. As a list of column titles which will be matched against accessors in the data:
A list of tuples with column titles and accessors:
As
Column
instances with a renderer. The renderer determines how a column is displayed and how the data is mapped to column properties such as text and icon:Now we can create our Table:
PR Checklist: