Skip to content
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

Conversation

vladimir-krestov
Copy link
Contributor

@vladimir-krestov vladimir-krestov commented Jan 15, 2020

Fixes #2717
Original bug: 604799: [Accessibility] TabPage has no keyboard tooltip

Proposed changes

  • Add a virtual IsHoveredWithMouse property to change its value in inherited classes
  • Change switch-case implementation in KeyboardToolTipStateMachine.cs due to extremely inconvenient use when debugging
  • Implement keyboard tooltips for TabPages. Now a Tab and a Page have one tooltip
  • Add a TabControl test app

Customer Impact

  • A user can get a tooltip using a keyboard

Regression?

  • No

Risk

  • Minimal

Screenshots

Before

  • TabPages have no keyboard tooltips
  • A TabPage tab has an incorrect tooltip text (TabControl text instead TabPage.ToolTipText property value)
  • A user gets only a TabControl tooltip using a Tab-navigation
    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
    Fixed

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

Test methodology

  • CTI
  • Manual UI testing

Test environment(s)

  • SDK Version: 5.0.100-alpha1-016143
  • Microsoft Windows [Version 10.0.18363.592]
Microsoft Reviewers: Open in CodeFlow

@vladimir-krestov vladimir-krestov requested a review from a team as a code owner January 15, 2020 12:18
@vladimir-krestov vladimir-krestov self-assigned this Jan 15, 2020
@vladimir-krestov
Copy link
Contributor Author

Have not tested yet

@RussKie RussKie added the 📭 waiting-author-feedback The team requires more information from the author label Jan 15, 2020
@ghost ghost removed the 📭 waiting-author-feedback The team requires more information from the author label Jan 22, 2020
@vladimir-krestov vladimir-krestov changed the title [Accessibility] Fixing TabPage keyboard tooltips WIP: [Accessibility] Fixing TabPage keyboard tooltips Jan 28, 2020
@codecov
Copy link

codecov bot commented Feb 12, 2020

Codecov Report

Merging #2719 into master will decrease coverage by 2.29143%.
The diff coverage is 38.2716%.

@@                 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
Flag Coverage Δ
#Debug 59.86899% <38.2716%> (-2.29144%) ⬇️
#production 32.42114% <27.0073%> (-0.94061%) ⬇️
#test 98.98365% <100%> (+0.00914%) ⬆️

@vladimir-krestov vladimir-krestov added the 📬 waiting-for-testing The PR is awaiting manual testing by the primary team; no action is yet required from the author(s) label Feb 13, 2020
@@ -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);
Copy link
Contributor Author

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:

  • Incorrect behavior when some different ToolTip instances are set:
    InternalItemsBug-DifferentInstances

  • Incorrect behavior when one ToolTip instance is set:
    InternalItemsBug-OneInstance

With this handler:

  • Expected work:
    InternalItemsToolTip

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

Copy link
Contributor

@weltkante weltkante Feb 13, 2020

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

Copy link
Contributor Author

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!

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@weltkante weltkante left a 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.

@ghost ghost added 📭 waiting-author-feedback The team requires more information from the author and removed 📭 waiting-author-feedback The team requires more information from the author labels Feb 17, 2020
@vladimir-krestov vladimir-krestov added the waiting-review This item is waiting on review by one or more members of team label Feb 17, 2020
@ghost ghost added 📭 waiting-author-feedback The team requires more information from the author and removed 📭 waiting-author-feedback The team requires more information from the author labels Feb 18, 2020
@@ -10971,6 +10971,19 @@ internal static IntPtr SetUpPalette(IntPtr dc, bool force, bool realizePalette)
return result;
}

protected virtual void SetToolTip(ToolTip toolTip, string toolTipText)
Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

@Tanya-Solyanik Tanya-Solyanik left a 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";
Copy link
Member

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");
Copy link
Member

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");
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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";
Copy link
Member

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));
Copy link
Member

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
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property overrides Control.IsHoveredWithMouse property, which related to IKeyboardToolTip.IsHoveredWithMouse() method.
image

I think it will be incorrect to make different names.

@@ -10931,6 +10931,19 @@ internal static IntPtr SetUpPalette(IntPtr dc, bool force, bool realizePalette)
return result;
}

protected virtual void SetToolTip(ToolTip toolTip, string toolTipText)
Copy link
Member

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.

Copy link
Contributor

@M-Lipin M-Lipin left a 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;
Copy link
Contributor

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.

Copy link
Contributor

@weltkante weltkante Feb 27, 2020

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Member

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;
Copy link
Contributor

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;
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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.

@vladimir-krestov vladimir-krestov marked this pull request as draft April 24, 2020 07:17
@vladimir-krestov
Copy link
Contributor Author

Will be reopened later

@ghost ghost added the draft draft PR label Sep 11, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Feb 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
draft draft PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Accessibility] TabPage has no keyboard tooltip
7 participants