-
Notifications
You must be signed in to change notification settings - Fork 988
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
WIP: [Accessibility] Fixing TabPage keyboard tooltips #2719
WIP: [Accessibility] Fixing TabPage keyboard tooltips #2719
Conversation
Have not tested yet |
src/System.Windows.Forms/src/System/Windows/Forms/KeyboardToolTipStateMachine.cs
Show resolved
Hide resolved
src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/TabControlTest.cs
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
|
@@ -1225,6 +1246,7 @@ public void SetToolTip(Control control, string caption) | |||
{ | |||
TipInfo info = new TipInfo(caption, TipInfo.Type.Auto); | |||
SetToolTipInternal(control, info); | |||
control.LostFocus += (object sender, EventArgs e) => Hide(control); |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, I will look at ways to fix these issues. Thank you!
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.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'll ask testers to test these changes using automated and manual testing.
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.
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
@@ -10971,6 +10971,19 @@ internal static IntPtr SetUpPalette(IntPtr dc, bool force, bool realizePalette) | |||
return result; | |||
} | |||
|
|||
protected virtual void SetToolTip(ToolTip toolTip, string toolTipText) |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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.
Looks good, please see my comments though
{ | ||
using TabPage tabPage = new TabPage(); | ||
tabPage.CreateControl(); | ||
string expected = "Some text"; |
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.
consider factoring this string out into a const field and using it everywhere in this file. Not necessarily in this same PR
{ | ||
public MyLabel() | ||
{ | ||
this.SetToolTip(new ToolTip(), "balas"); |
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.
please remove this
qualifier
public TabControlTest() | ||
{ | ||
InitializeComponent(); | ||
//toolTip.SetToolTip(tabControl1, "TabControl"); |
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.
please remove the commented out line
|
||
private void ButtonClick(object sender, EventArgs e) | ||
{ | ||
// It will be removed after testing |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
toolTip.SetToolTip(tabPage1, "This is page 1"); | ||
break; | ||
case 2: | ||
tabPage2.ToolTipText = "2) Some tt text"; |
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.
tt-> tooltip
CheckNativeToolTip(control); | ||
CheckCompositeControls(control); | ||
|
||
control?.SetToolTipInternal(this, GetToolTip(control)); |
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.
on this line we know that control is not null be cause cast on line 644 succeed
} | ||
} | ||
|
||
private protected override bool IsHoveredWithMouse |
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.
Consider a simplified name - IsHovered
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.
@@ -10931,6 +10931,19 @@ internal static IntPtr SetUpPalette(IntPtr dc, bool force, bool realizePalette) | |||
return result; | |||
} | |||
|
|||
protected virtual void SetToolTip(ToolTip toolTip, string toolTipText) |
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 need to have xml-docs, so they will flow into the docs.
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.
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.
{ | ||
if (!IsHandleCreated || toolTip == null) | ||
{ | ||
return; |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -10931,6 +10931,19 @@ internal static IntPtr SetUpPalette(IntPtr dc, bool force, bool realizePalette) | |||
return result; | |||
} | |||
|
|||
protected virtual void SetToolTip(ToolTip toolTip, string toolTipText) |
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.
I see that toolTipText
is not used in some implementation. Please use default value for this.
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.
I'll do, thanks!
this.toolTipCaption = toolTipCaption; | ||
if (toolTip == null) | ||
{ | ||
return; |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good call.
We'll need to record this in dotnet/docs#17085
{ | ||
if (toolTip == null || !ShowToolTips) | ||
{ | ||
return; |
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.
The same as previous: arguments check.
{ | ||
if (toolTip == null) | ||
{ | ||
return; |
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.
The same as previous: argument check.
} | ||
|
||
// Show the same tooltip for the page's tab. | ||
if (toolTipText != null && ToolTipText != toolTipText) |
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.
The test ToolTipText != toolTipText
is already in the property setter implementation.
{ | ||
if (toolTip != null) | ||
if (toolTip == null || !ShowNodeToolTips) |
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.
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.cs
due 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