Tk Source Code

View Ticket
Login
Ticket UUID: 300bad1beb66902b3fcb9afc158b4aa9b6d4c6e2
Title: Using tab to cycle through tkinter widgets breaks foreground styling
Type: Bug Version:
Submitter: watusimoto Created on: 2021-11-16 17:38:26
Subsystem: 07. [entry] Assigned To: marc_culler
Priority: 5 Medium Severity: Minor
Status: Open Last Modified: 2021-11-30 20:02:20
Resolution: None Closed By: nobody
    Closed on: 2021-11-30 19:47:59
Description:
Attached is a slightly modified example of the code sample in Bryan Oakley's response to:

https://stackoverflow.com/questions/30337351/how-can-i-ensure-my-ttk-entrys-invalid-state-isnt-cleared-when-it-loses-focus

I have modified it by changing the style.map to change the widget's foreground color (rather than background), and by adding a button below the text input.

Typing a string containing "invalid" into the textbox causes it to fail validation (its state is shown in the bottom label, updated every second).  As expected, the foreground changes to red.

However, if I use [tab] to toggle between the elements, the foreground of the text entry gets changed to black, even if the item is invalid.

Adding         
    style.map("TEntry",  selectforeground=[('invalid', "green")])
creates a new set of weirdness, where the invalid text becomes green when tabbing through the widgets.

I'll add that when the red text incorrectly turns black, on my display, I can see a very subtle red halo, as if the text is being printed first as red, then as black in a second pass.


I'm running on Windows 10, Python 3.83.  This may be a Windows only bug.

=====


import tkinter as tk
import tkinter.ttk as ttk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        # give invalid entries a red background
        style = ttk.Style()
        style.map("TEntry",  foreground=[('invalid', "red")])
        # style.map("TEntry",  selectforeground=[('invalid', "green")])     # More weirdness when uncommented

        self.entryVar = tk.StringVar()
        self.entry = ttk.Entry(self, textvariable=self.entryVar)
        self.button = ttk.Button(self, text="Button")

        # this label will show the current state, updated
        # every second.
        self.label = tk.Label(self, anchor="w")
        self.after_idle(self.updateLabel)

        # layout the widgets
        self.entry.pack(side="top", fill="x")
        self.button.pack(side="top", fill="x")
        self.label.pack(side="bottom", fill="x")

        # add trace on the variable to do custom validation
        self.entryVar.trace("w", self.validate)

        # set up bindings to also do the validation when we gain
        # or lose focus
        self.entry.bind("<FocusIn>", self.validate)
        self.entry.bind("<FocusOut>", self.validate)

    def updateLabel(self):
        '''Display the current entry widget state'''
        state = str(self.entry.state())
        self.label.configure(text=state)
        self.after(1000, self.updateLabel)

    def validate(self, *args):
        '''Validate the widget contents'''
        value = self.entryVar.get()
        if "invalid" in value:
            self.entry.state(["invalid"])
        else:
            self.entry.state(["!invalid"])

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
User Comments: marc_culler (claiming to be Marc Culler) added on 2021-11-30 20:02:20:
Reopening this ticket (now that we understand what it is about).

marc_culler (claiming to be Marc Culler) added on 2021-11-30 19:47:59:
I think the culture is that we are not trigger-happy with not-a-bugs
and are very accepting of good ideas, especially if they are improvements.
Making behaviors of Ttk widgets match those of the host is a goal.  On
the other hand, a patch is much more likely to be quickly accepted than
a proposal which requires someone else to drop what they are doing and
work on a different project.

watusimoto added on 2021-11-30 19:26:34:
The idea of manually clearing the selection is almost right; after some experimentation, I found you want to clear the selection when another widget gets focus, rather than when the widget in question loses it.  If you go the "lose it" route, you lose your selection when changing to a different application.

The (slight) downside is that you still need to set the selection foreground for all the invalid !focus and !invalid focus, which just feels wrong.  The upside is that seems to fix the double-rendering issue I mentioned in passing, except when another app gets the focus, but I'm just not going to worry about that.  To make this work in production, I'll need to do some higher level widget management to systematically handle focus across multiple widgets, because each non-Entry widget will have to listen for focus in and clear the selection in any Entry widgets.  Ugly, but doable.

So it does appear it can be made to work "normally" (for Windows, anyway), but I can't believe that this is all an intentional design.  It still feels there is a bug in here somewhere, probably in the managing of selection when changing from a Entry to a Button (for example) on Windows.

I don't know enough about the culture of this project to know how to proceed.  I have a work-around.  I can try to rework this into a different bug report, but am not sure how to concisely explain the problem in such a way that it won't be dismissed.  We could push this one forward with perhaps a summary of the issue, but including all the background of how we got here.  Or I could just let it go.

If a pursing this is just going to get "not-a-bugged", I'll declare a personal victory and go home, leaving the next person who tries to do something similar to find their own way through the wilderness.

What do you think?

marc_culler (claiming to be Marc Culler) added on 2021-11-30 14:46:03:
| Presumably this does not apply when tabbing from one Entry widget to
| another;

You may be right, and of course you could make your widgets behave that
way by clearing the selection when a widget loses focus.  In the case
of macOS I don't think this is discussed in the Human Interface
Guidelines, but my experiments indicate that selection is maintained
per toplevel but not per widget.  That is, switching between toplevels
does not change the selection status, although it does change what Tk
would call the selectbackground color.  So, in Aqua, the selectbackground
should depend on the background state.  Tabbing out of a text entry
widget removes the selection indication, although if you continue
tabbing until the entry returns to focus then the previously selected
text is reselected.  So in Aqua, the selectbackground should also depend
on the focus state.  On the other hand, tabbing out of a listview does
not deselect the selected item, it just changes the selectbackground
color of the selected item.  It is not clear to me why this inconsistency
exists.

I do think it would make sense to replicate this behavior in the Aqua theme
and to replicate the Windows behavior in the Windows theme.  It seems like
a detail which could easily have been missed over the years.

watusimoto added on 2021-11-29 21:13:31:
===
It does make sense to me that the selection status should remain unchanged  when an Entry loses focus, since the selection is done per widget. 
===

Presumably this does not apply when tabbing from one Entry widget to another; that is, a selection in a second Entry removes a selection in the first.  This explains another set of (previously inexplicable) behavior I observed when trying to nail this issue down, where the type of widget "receiving" the focus impacted whether the text stayed turned red or black, as well as when one Entry widget received focus, the other changed from black to red.

marc_culler (claiming to be Marc Culler) added on 2021-11-29 20:52:32:
Thanks. It is fine to keep commenting on a closed ticket as far as I know.

I was only testing on macOS, which does not deselect the text when the
Entry loses focus.  I don't know whether Windows actually deselects
the text.  However, I can see that Windows removes the visual indicator
that the text is selected.  In winTheme.tcl I see this mapping:

ttk::style map TEntry \
            -fieldbackground \
                [list readonly SystemButtonFace disabled SystemButtonFace] \
            -selectbackground [list !focus SystemWindow] \
            -selectforeground [list !focus SystemWindowText] \
            ;

That is instructing Ttk to use the SystemWindow color as the selectbackground
when the Entry does not have focus.  I have no idea why that choice was made.
My suspicion is that Tk still knows that the text is selected, but I am not
sure. It does make sense to me that the selection status should remain unchanged
when an Entry loses focus, since the selection is done per widget. But I can't
claim to have a really strong argument for doing that and I don't know why that
choice was made either.

watusimoto added on 2021-11-29 20:28:54:
[[[ Sorry to keep adding to this ticket; I'd rather respond inline but can't figure out how to do it.  If what I'm doing is improper, please correct me. ]]]


===
Step 4. (focus, invalid)  selectforeground   white
Step 5. (!focus, invalid) selectforeground   black
Step 6. (focus, invalid)  foreground         red

Since you did not specify any rules for the selectforeground color, in Step 5 the selectforeground color should be the default for a non-focused entry widget.
===

In step 5, the Entry widget no longer has focus, and the selection goes away (as evidenced by the text no longer being displayed with the selectbackground color).  Why would the selectforeground color still be used for rendering?  

That said, I was able to resolve the issue by adding these two styles:

        selectforeground=[
                        (["invalid", "!focus"], "red"),
                        (["!invalid", "!focus"], "black"),
                    ],


Still some rendering issues, but those aren't in scope for this ticket.
(see https://i.imgur.com/T02AMf2.png, and notice the different quality of the reds; clearly some double-rendering going on).

So I guess the issue boils down to text staying selected when the entry widget loses focus.  I can't see the case for that, but I'm not sure I can argue that it is strictly a bug, especially when I can apply styles to work around it.

If this is indeed intended behavior, I'd be curious as to why it is so, but will accept the closing of this ticket.

Thanks for your help!

marc_culler (claiming to be Marc Culler) added on 2021-11-27 05:17:21:

The color of the text is determined by two things: the state of the entry and whether the color is the foreground or the selectforeground color.

I think that the state and the color type are as follows:

        State             Color type         observed

Step 1: (focus)           foreground         black
Step 2. (focus, invalid)  foreground         red
Step 3. (!focus, invalid) foreground         red
Step 4. (focus, invalid)  selectforeground   white
Step 5. (!focus, invalid) selectforeground   black
Step 6. (focus, invalid)  foreground         red

Since you did not specify any rules for the selectforeground color, in Step 5 the selectforeground color should be the default for a non-focused entry widget.

According to winTheme.tcl, the default selectforeground color for all widgets is SystemHighlightText, which I assume is white, and for a TEntry widget the style map overrides that default with the line: -selectforeground [list !focus SystemWindowText] which, assuming that SystemWindowText is black, should mean that the selectforeground color for a non-focused entry is black by default.

So it still seems to me that you are seeing the expected behavior.


watusimoto added on 2021-11-27 03:17:26:
"The point seems to be that when an entry is focused by tab traversal
(as opposed to, say, clicking on the entry) then all of the text in the
entry is automatically selected.  The color of selected text is set
with the selectforeground option, not the foreground option."

This is expected, and is not the behavior I am describing.

1. The text entry text color is black.
2. Enter "invalid" into the text entry causes the field to be marked as invalid, and the text color is changed to red.
3. Hitting tab changes focus to the button; the text is still red.
4. Hitting tab again changes the focus back to the entry field; it is selected with white foreground and blue background.
5. Hitting tab again changes the focus to the button again, but now the text is black and not red.
6. Clicking into the text entry causes the text to become red again. 

It is step 5 that I contend is a bug.  When tabbing out of the text entry box, the text should become red, not black.

marc_culler (claiming to be Marc Culler) added on 2021-11-23 21:26:21:
Closing this ticket as invalid.

marc_culler (claiming to be Marc Culler) added on 2021-11-22 14:36:30:
This is not a Windows-only bug.  I see the same behavior on macOS.

But I don't think it is actually a bug.  I get the expected behavior
if I replace your call to style.map with the following:

style.map("TEntry",
            foreground=[('invalid', 'red')],
            selectforeground=[('invalid', 'red')])

The point seems to be that when an entry is focused by tab traversal
(as opposed to, say, clicking on the entry) then all of the text in the
entry is automatically selected.  The color of selected text is set
with the selectforeground option, not the foreground option.

The "extra weirdness" does not seem to be so weird to me.  The only thing
that might be unexpected is that the now-selected text remains selected
when focus moves to the button.