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. |
