Column data styles for Table and Tree widgets#4258
Column data styles for Table and Tree widgets#4258corranwebster wants to merge 18 commits intobeeware:mainfrom
Column data styles for Table and Tree widgets#4258Conversation
This reverts commit 20bac9d.
Also, properly scale fonts.
johnzhou721
left a comment
There was a problem hiding this comment.
Mainly looks good. A couple of typos and nits, and 2 places where I feel we may be able to deduplicate the code from a design perspective.
| def text_color(self, row): | ||
| value = self.value(row) | ||
| if value < 0: | ||
| return Color.parse("#ff0000) |
There was a problem hiding this comment.
| return Color.parse("#ff0000) | |
| return Color.parse("#ff0000") |
| @@ -0,0 +1 @@ | |||
| The `Table` and `Tree` widgets on Cocoa and Qt, and the `Table` widget on Android, can now specify colors, fonts and text alignment to use for individual cells based on the data contained in the cell by writing an appropriate `Column` subclass. | |||
There was a problem hiding this comment.
A nit for this to read more smoothly:
| The `Table` and `Tree` widgets on Cocoa and Qt, and the `Table` widget on Android, can now specify colors, fonts and text alignment to use for individual cells based on the data contained in the cell by writing an appropriate `Column` subclass. | |
| The `Table` and `Tree` widgets on Cocoa and Qt and the `Table` widget on Android can now specify colors, fonts, or text alignment for individual cells based on their data by providing an appropriate `Column` subclass. |
| if text_align is not None: | ||
| tcv.textField.alignment = NSTextAlignment(text_align) | ||
| else: | ||
| tcv.textField.alignment = self.alignment | ||
|
|
||
| if color is not None: | ||
| tcv.textField.textColor = native_color(color) | ||
| else: | ||
| tcv.textField.textColor = None | ||
| if background_color is not None: | ||
| tcv.drawsBackground = True | ||
| tcv.backgroundColor = native_color(background_color) | ||
| else: | ||
| tcv.textField.drawsBackground = False | ||
|
|
||
| # font is only None if something is very wrong (eg. can't find system font) | ||
| # so can't test | ||
| if font is not None: # pragma: no branch | ||
| tcv.textField.font = font._impl.native |
There was a problem hiding this comment.
This piece of code gets thrown around a lot. Is it possible to have a set_textfield_attributes function at the top of this file that accepts all the stylistic attributes, and reuse this in tree.py?
| stacklevel=2, | ||
| ) | ||
|
|
||
| # currently only handle icons and text |
| stacklevel=2, | ||
| ) | ||
|
|
||
| # currently only handle icons and text |
| if role == Qt.ItemDataRole.DecorationRole: | ||
| icon = column.icon(node) | ||
| if icon is not None: | ||
| return icon._impl.native | ||
| elif role == Qt.ItemDataRole.DisplayRole: | ||
| return column.text(node, self._missing_value) | ||
| elif role == Qt.ItemDataRole.TextAlignmentRole: | ||
| text_align = column.text_align(node) | ||
| if text_align is not None: | ||
| print("here", text_align, qt_text_align(text_align, CENTER)) | ||
| return qt_text_align(text_align, CENTER) | ||
| elif role == Qt.ItemDataRole.ForegroundRole: | ||
| color = column.color(node) | ||
| if color is not None: | ||
| return native_color(color) | ||
| elif role == Qt.ItemDataRole.BackgroundRole: | ||
| color = column.background_color(node) | ||
| if color is not None: | ||
| return native_color(color) | ||
| elif role == Qt.ItemDataRole.FontRole: | ||
| font = column.font(node, self._font_data) | ||
| # font is only None if something is very wrong (eg. can't find | ||
| # system font) so can't test | ||
| if font is not None: # pragma: no branch | ||
| return font._impl.native | ||
| except Exception: # pragma: no cover | ||
| logger.exception( | ||
| f"Could not get data for node {node}, column {column_index}" |
There was a problem hiding this comment.
I suggest having an abstraction for this whole block, like a function that returns correct value for a Qt role and a Toga column object, so we can reuse it in both table and tree files.
| elif role == Qt.ItemDataRole.TextAlignmentRole: | ||
| text_align = column.text_align(node) | ||
| if text_align is not None: | ||
| print("here", text_align, qt_text_align(text_align, CENTER)) |
There was a problem hiding this comment.
Pragmatically, this looks like a good implementation of style-modifying behaviours - but I'm not sure how I feel about the core idea that the API is proposing.
We've almost completely avoided having public APIs for style modifying features. I can't think of any outside of Canvas, and in that context it's exposing very specific drawing instructions that aren't really "style" per se.
While direct "change font" etc APIs are a pragmatic solution, this would be a fairly radical change to Toga's API. While a lot of these API implementations makes sense (and would likely be required in any table styling API) on the backend, I'm not sure they makes sense as a public interface.
I'd much rather spend some time getting a design for #4261 #4238 sorted out, rather than implement a short-term solution that we then need to deprecate because we find a clean style-based API.
(EDIT: Corrected the ticket reference)
So the intent of this really isn't "styles" per-se either, which is what I was trying to get at with the distinction of "data-driven styling" - this is for handling the situation where things like color come from the data itself, rather than from the styling of the application. The most direct example of this would be a column where the value for each row is a Similarly, a list of fonts, where sample text is displayed in the font. But the motivating examples for me are from data visualisation as noted (colormaps, highlighting outliers, etc.). EDIT: the distinction here is "if the data changes, does the 'style' change" (so updates come via the usual data source mechanisms).
There's definitely the possibility that this is the wrong API: for example it might be that rather than the individual bits of a "style" there should be just one "row_style" method which returns back style data in some usable format for the backend. That's more difficult to use for someone who only wants to change one aspect of the style, but it may be more in-line with the existing APIs. Or in a future with a more complete CSS implementation, there's just a Or the use-cases mentioned gain separate explicit support (eg. you want to colormap values, then the column has a "colormap" attribute which takes appropriate arguments and then applies them to the row values internally to get the colors to use. This API is more-or-less the dumbest thing that could work and be implemented incrementally.
I implemented this, because I don't see it as a stop-gap or a short-term solution that would be deprecated later. Rather column styling (as discussed tangentially in #4238) would get slotted in behind this API as a default behaviour, something like (for background color, say):
So columns which want to do fancy data-based colouring can override it completely, but most columns would just get the style background, and the opportunity is there to do something which takes into account the default and modify it (eg. by highlighting outliers with a darker shade of the style's background color). I agree that we'd like to avoid a situation where people start using this to do things which should be done via styling, but I think it's a fairly small lift from this PR to adding styling in the sorts of ways discussed in #4238 - it's more or less just adding the default behaviour plus a way of notifying the table implementation to re-draw when the column style changes. |
The use case of "data informs style" definitely makes sense. However, I'm not sure it follows that the API should expose options for that directly. The separation of content and form is something fairly fundamental to most presentation mechanisms from the last 30 years:
Honestly, I think it's mostly the fact that there are explicit
So... you say that... but is it actually that from a full fix for #4238? Here's my thinking: the API you've implemented here essentially relies on the fact that a change in a value will trigger a redraw. If the thing that is grating for me is the existence of a For me, that's a lot closer to what I'd anticipate as the final form of #4238-esque style descriptions - adding a However, even if we only do the first half (i.e., add a |
But it's also not, because of the kwargs on widgets. I can say: text_field = TextInput("my_text", text_align=RIGHT, font=...)rather than text_field = TextInput("my_text", style=Style(text_align=RIGHT, font=...))it's just that to access those properties you need to go through the style object.
Nope! This doesn't fix #4238, and isn't intended to, this is solving a different problem (where the color, font, etc. is the data). But I could fold in a fix for #4238 into this, particularly if we leave the problem of updating styles for later.
So if I understand, I think you'd prefer a final API (which partially solves #4238) more like: Which is definitely do-able and not a huge change from what I have here if we don't worry about style updates (ie. the default style is set at column creation time and not changed). I think the API should call it
My gut feeling is that this may be a bit more cumbersome to work with than fine-grained methods (ie. I just want to set the background color of this cell to highlight an outlier, but now I need to worry about dealing a whole style object), and I'll play around with this and see what it looks like. |
Sure - that's more of a "convenience of construction" thing, though. As you note, they're not reflected for access - it's purely a
Yes - that's the idea.
I hadn't thought of that aspect - but it's a fair point.
Hrm... I wonder if that means we ultimately end up with a
Fair - but they can also be shared - so you could have your 3 "color" styles for red, green, and blue cells, and return those instances based on the value of the row. |
I spent some time with this and I'm running into a mismatch between the way styles work and the way that we need to use them to get cell styles. If I understand the abstraction, you're never supposed to read the attributes off of the style objects directly (because the actual attributes used aren't part of the base API), rather the styles are designed to be set, and the process of setting calls the applicator object which then pushes the changes onto the widget via the widget API's The style information used in cell styling is much more ephemeral: in most cases we'd be creating styles on the fly with no applicator, and what we really want to do is read off the style attributes that we care about. To follow the intended usage, I think I'd need to create a little "dummy" applicator class like: class DummyApplicator:
def set_text_align(self, text_align):
setattr(self, "text_align", text_align)
def set_font(self, font):
...
# etcand then read off the values, something like: style = column.cell_style(row)
applicator = DummyApplicator()
style.applicator = applicator
# now applicator.values has what we want, eg. applicator.text_alignFor some of the backends we could possibly even write custom ephemeral applicators that also include the logic for setting the appropriate style attributes (or have some sort of ephemeral Cell implementation object as a target) which would work well for Android say, but for efficient item models (such as Qt, or if we wrote a data model for Gtk, I think) we will end up computing the whole style and applying the applicator over and over again for all the values each time it wants to get just one style component for a cell because of the way that it queries data for cells (although, to be fair, this is I think going to be the case for Qt no matter what: Qt item models work by Qt asking for a particular data "role" for the cell (eg. text, color, alignment, etc.) one-by-one so any API which is just providing a "style" object will have the style called multiple times for every visible cell in the table). So I can make it work with Travertino styles, but it seems... complex. Also, if we have styles for the whole column, then we're possibly going to need an applicator that sets those style values somewhere, and the logical place would be the column object, so the column objects might start growing I'll think a bit more about this, but any thoughts (or pointing out where my mental model is wrong) would be useful. |
That's the general idea, yes. The underlying concept is that way you describe style relationships doesn't have to be a 1-1 mapping to the way those styles are manifested. This is most important when it comes to size-based information, and less obvious for things like font faces.
It doesn't necessarily have to be a standalone applicator class (although that's definitely an option); there just has to be a class that
I'll definitely concede it's a lot more convoluted than "just set a font". In
My read on this would be that this where we need to start considering how cascading works. Although we might be defining table-level, column-level and cell-level style definitions, a specific cell has a single style that is the cascaded set of all of them. The cell renderer would need to compute that cascade, and then apply the style as part of the rendering process. And yes - that means that somewhere in the chain there will be "set_*" methods, as that's the interface that the applicator requires.
It sounds like your mental model is correct; how to make that model fit is definitely the complicated question here. |
This PR adds support for data-driven styling of Table and Tree cells to allow support for things like value-based background colors for numerical cells (heatmaps/colormaps), highlighting of outliers or errors, right-aligning of numeric values and so on. This PR implements the functionality for Cocoa, Qt and Android backends.
Currently this supports text alignment, foreground and background colors, and fonts.
This PR specifically doesn't include:
AccessorColumnto support styling from the data, eg. allowing(icon, text, style)values (design is needed) See Style Information inAccessorColumns#4261.DetailedList(this would make sense, but would needDetailedListto useColumnobjects or something similar).For the other backends:
Known issues:
Ref #1066 and #3760.
After this is merged it might be that we can consider #1066 finished.
PR Checklist: