WIP: [Accessibility] Fixing TabPage keyboard tooltips#2719
WIP: [Accessibility] Fixing TabPage keyboard tooltips#2719vladimir-krestov wants to merge 24 commits intodotnet:masterfrom
Conversation
|
Have not tested yet |
src/System.Windows.Forms/src/System/Windows/Forms/KeyboardToolTipStateMachine.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/TabControlTest.cs
Outdated
Show resolved
Hide resolved
06ce4fe to
b482c78
Compare
Codecov Report
@@ Coverage Diff @@
## master #2719 +/- ##
===================================================
- Coverage 62.16042% 59.86899% -2.29143%
===================================================
Files 1259 1241 -18
Lines 449450 431873 -17577
Branches 39227 38826 -401
===================================================
- Hits 279380 258558 -20822
- Misses 164590 167936 +3346
+ Partials 5480 5379 -101
|
There was a problem hiding this comment.
I added this handler to fix a tooltip showing for a control when it loses focus.
Without this handler:
With this handler:
@RussKie, @M-Lipin, @Tanya-Solyanik, @JuditRose, @hughbe, @weltkante, could you please look and say what negative effect this might give for other controls and tooltips? I did not find any bugs.
There was a problem hiding this comment.
Well, I recognize the behavior you describe and try to fix, I have seen it before in our applications but I never digged down to find its root cause. I don't know if the behavior was intended, so with your change you could either be breaking intended behavior or fixing a bug.
Either way your implementation is incorrect and can't be left this way, you are subscribing within a public API without ever unsubscribing, so multiple calls will accumulate event handlers. The typical pattern is unsubscribing and resubscribing - the first unsubscribe cleans up previous subscriptions (no-op if none exist). However since you are using a lambda capturing the control as a closure variable I don't know if this pattern works here. (I don't know how equality works when closures are in play, I'd expect each closure to be distinct so to unsubscribe you'd have to save the closure away, but I may be mistaken. If you want to test it you'd have to check that its not called multiple times after multiple SetToolTip calls by putting breakpoints or debug output into the LostFocus handler. You'd also have to check that calling SetToolTip on multiple controls doesn't unsubscribe each other.)
There was a problem hiding this comment.
Good points, I will look at ways to fix these issues. Thank you!
There was a problem hiding this comment.
@weltkante, I changed the implementation. Subscription and Unsubscription happen once now when setting a tooltip many times, I checked. Please review.
There was a problem hiding this comment.
Also, I'll ask testers to test these changes using automated and manual testing.
weltkante
left a comment
There was a problem hiding this comment.
The approach looks right but might need some more fine-tuning, I think you are still double-subscribing.
src/System.Windows.Forms/src/System/Windows/Forms/TabControl.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
I used protected for SetToolTip method thereby adding API for developers and they can change this implementation.
This is correct or I need to hide this protected API using the private protected modifier?
/cc: @RussKie, @Tanya-Solyanik, @merriemcgaw
There was a problem hiding this comment.
Any custom control must be able to hook into the tooltip display chain to override the display logic, if necessary. With that, I believe, should be made protected.
There was a problem hiding this comment.
This change to the public surface will not require designer changes, I don't see any problems with it.
There was a problem hiding this comment.
How would you explain, when to use Tooltip.SetToolTip and when to use Control.SetToolTip?
Perhaps this method can be named differently?
There was a problem hiding this comment.
That's the thing, I can't see any real benefit of using one over the other.
myToolTip.SetToolTip(myControl, toolTipText) calls into myControl.SetToolTip(myToolTip, toolTipText) which recurses.
I can guess the original idea of ToolTip was to extend arbitrary controls. However then a number of controls decided to displaying tooltips in their own way (e.g. Label when its text is ellipsised) by keeping own instances of ToolTip and exposing properties like ToolTipText. So now we have an inconsistent devex because for some controls a developer must use an instance of own ToolTip, and for some other control - not.
With this API cleanup we further blurring the line and removing reasons to use ToolTip control. Instead making it a little more consistent experience - each Control allows setting its tooltips in a common manner. There be 🐉 🐉 though (e.g. the Label's case) that we'll likely need to resolve. We touched on these with @vladimir-krestov and he was meant to raise issues for tracking purposes.
Also now it may be possible to reduce number of instances of ToolTip, have one global singleton per app and pass it to all controls that require tooltips.
Now we can probably take this further and question whether we can have a single instance of a ToolTip provider/renderer. But that's a whole separate discussion.
e6081ad to
378d6ea
Compare
Tanya-Solyanik
left a comment
There was a problem hiding this comment.
Looks good, please see my comments though
There was a problem hiding this comment.
consider factoring this string out into a const field and using it everywhere in this file. Not necessarily in this same PR
There was a problem hiding this comment.
please remove this qualifier
There was a problem hiding this comment.
please remove the commented out line
There was a problem hiding this comment.
If this was useful for feature development, it will be also useful when debugging this feature. Are there any problems with keeping this test?
There was a problem hiding this comment.
The project "MultipleControls" test app already has TabControl inside for testing so I planned to remove the current app after the fix. Do you think we need a separate test app for TabControl to test TabPage tooltips work? If so, I'll refactor it later.
There was a problem hiding this comment.
on this line we know that control is not null be cause cast on line 644 succeed
There was a problem hiding this comment.
Consider a simplified name - IsHovered
src/System.Windows.Forms/src/System/Windows/Forms/UpDownBase.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
We need to have xml-docs, so they will flow into the docs.
M-Lipin
left a comment
There was a problem hiding this comment.
Looks good to me. There are several minor points to refactor and the point about tight coupling between Controls and ToolTips: Control references ToolTips in Control.SetToolTip() method and ToolTip references Control in ToolTip.SetToolTip() method. It would be better to refactor it to reduce coupling.
There was a problem hiding this comment.
Why return;? It seems to me that method should call CreateHandle(); in case IsHandleCreated == false and should throw an ArgumentNullException in case toolTip == null.
There was a problem hiding this comment.
Definitely not, you must not call CreateHandle when setting a tooltip, this would cause handle instantiation in the constructor during InitializeComponents before all designer properties have been set. The rest of the tooltip infrastructure has logic to not create handles and store the tooltip somewhere else when the handle is not created.
If anything you could throw an exception if this code path is taken when no handle is available, because it would be a bug in the internal logic.
There was a problem hiding this comment.
I see that toolTipText is not used in some implementation. Please use default value for this.
There was a problem hiding this comment.
I'll do, thanks!
There was a problem hiding this comment.
It seems that call SetToolTip() with toolTip = null is not correct and we should throw ArgumentNullException. Maybe argument checks can be added to the base implementation and overridden methods should call base.SetToolTip() before the remaining implementation.
There was a problem hiding this comment.
That's a good call.
We'll need to record this in dotnet/docs#17085
There was a problem hiding this comment.
The same as previous: arguments check.
There was a problem hiding this comment.
The same as previous: argument check.
There was a problem hiding this comment.
The test ToolTipText != toolTipText is already in the property setter implementation.
There was a problem hiding this comment.
Check `toolTip' for null in base method.
378d6ea to
3d6baa8
Compare
|
Will be reopened later |




Fixes #2717
Original bug: 604799: [Accessibility] TabPage has no keyboard tooltip
Proposed changes
KeyboardToolTipStateMachine.csdue to extremely inconvenient use when debuggingCustomer Impact
Regression?
Risk
Screenshots
Before
After
TabPages have keyboard tooltips
TabPage tab has the same tooltip as a page. If set a new tooltip or change ToolTipText property value it will affect both tooltips. It is a simulation of that a tab tooltip and a page tooltip are one united tooltip

A user can get TabControl tooltip if this TabControl has no pages

Test methodology
Test environment(s)
Microsoft Reviewers: Open in CodeFlow