Skip to content

Commit

Permalink
Support keyboard tooltips in TreeNode (#4466)
Browse files Browse the repository at this point in the history
  • Loading branch information
SergeySmirnov-Akvelon authored Feb 22, 2021
1 parent 661239d commit 77dcb4f
Show file tree
Hide file tree
Showing 12 changed files with 870 additions and 24 deletions.
2 changes: 2 additions & 0 deletions src/System.Windows.Forms/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
~override System.Windows.Forms.TreeView.OnGotFocus(System.EventArgs e) -> void
~override System.Windows.Forms.TreeView.OnLostFocus(System.EventArgs e) -> void
3 changes: 3 additions & 0 deletions src/System.Windows.Forms/src/System/Windows/Forms/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14061,6 +14061,9 @@ static bool IsKeyDown(Keys key, ReadOnlySpan<byte> stateArray)
}
}

internal virtual ComCtl32.ToolInfoWrapper<Control> GetToolInfoWrapper(ComCtl32.TTF flags, string caption, ToolTip tooltip)
=> new ComCtl32.ToolInfoWrapper<Control>(this, flags, caption);

private readonly WeakReference<ToolStripControlHost> toolStripControlHostReference
= new WeakReference<ToolStripControlHost>(null);

Expand Down
11 changes: 11 additions & 0 deletions src/System.Windows.Forms/src/System/Windows/Forms/ListView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3503,6 +3503,17 @@ internal int GetNativeGroupId(ListViewItem item)
}
}

internal unsafe override ComCtl32.ToolInfoWrapper<Control> GetToolInfoWrapper(TTF flags, string caption, ToolTip tooltip)
{
// The "ShowItemToolTips" flag is required so that when the user hovers over the ListViewItem,
// their own tooltip is displayed, not the ListViewItem tooltip.
var wrapper = new ComCtl32.ToolInfoWrapper<Control>(this, flags, ShowItemToolTips ? null : caption);
if (ShowItemToolTips)
wrapper.Info.lpszText = (char*)(-1);

return wrapper;
}

internal void GetSubItemAt(int x, int y, out int iItem, out int iSubItem)
{
var lvhi = new LVHITTESTINFO
Expand Down
18 changes: 1 addition & 17 deletions src/System.Windows.Forms/src/System/Windows/Forms/ToolTip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -991,14 +991,7 @@ private unsafe ToolInfoWrapper<Control> GetTOOLINFO(Control control, string capt
flags |= TTF.RTLREADING;
}

bool noText = (control is TreeView tv && tv.ShowNodeToolTips)
|| (control is ListView lv && lv.ShowItemToolTips);

var info = new ToolInfoWrapper<Control>(control, flags, noText ? null : caption);
if (noText)
info.Info.lpszText = (char*)(-1);

return info;
return control.GetToolInfoWrapper(flags, caption, this);
}

private ToolInfoWrapper<IWin32WindowAdapter> GetWinTOOLINFO(IWin32Window hWnd)
Expand Down Expand Up @@ -1995,15 +1988,6 @@ private void WmMove()
return;
}

// Treeview handles its own ToolTips.
if (window is TreeView treeView)
{
if (treeView.ShowNodeToolTips)
{
return;
}
}

// Reposition the tooltip when its about to be shown since the tooltip can go out of screen
// working area bounds Reposition would check the bounds for us.
var r = new RECT();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Drawing;

namespace System.Windows.Forms
{
public partial class TreeNode : IKeyboardToolTip
{
bool IKeyboardToolTip.AllowsChildrenToShowToolTips() => AllowToolTips;

bool IKeyboardToolTip.AllowsToolTip() => true;

bool IKeyboardToolTip.CanShowToolTipsNow() => AllowToolTips;

string IKeyboardToolTip.GetCaptionForTool(ToolTip toolTip) => ToolTipText;

Rectangle IKeyboardToolTip.GetNativeScreenRectangle() => RectangleToScreen(Bounds);

IList<Rectangle> IKeyboardToolTip.GetNeighboringToolsRectangles()
{
TreeNode nextNode = NextVisibleNode;
TreeNode prevNode = PrevVisibleNode;
List<Rectangle> neighboringRectangles = new List<Rectangle>();

if (nextNode is not null)
{
neighboringRectangles.Add(RectangleToScreen(nextNode.Bounds));
}

if (prevNode is not null)
{
neighboringRectangles.Add(RectangleToScreen(prevNode.Bounds));
}

return neighboringRectangles;
}

IWin32Window IKeyboardToolTip.GetOwnerWindow() => TreeView;

bool IKeyboardToolTip.HasRtlModeEnabled() => TreeView?.RightToLeft == RightToLeft.Yes;

bool IKeyboardToolTip.IsBeingTabbedTo() => Control.AreCommonNavigationalKeysDown();

bool IKeyboardToolTip.IsHoveredWithMouse() => TreeView?.AccessibilityObject.Bounds.Contains(Control.MousePosition) ?? false;

void IKeyboardToolTip.OnHooked(ToolTip toolTip) => OnKeyboardToolTipHook(toolTip);

void IKeyboardToolTip.OnUnhooked(ToolTip toolTip) => OnKeyboardToolTipUnhook(toolTip);

bool IKeyboardToolTip.ShowsOwnToolTip() => AllowToolTips;

private bool AllowToolTips => TreeView?.ShowNodeToolTips ?? false;

internal virtual void OnKeyboardToolTipHook(ToolTip toolTip) { }

internal virtual void OnKeyboardToolTipUnhook(ToolTip toolTip) { }

private Rectangle RectangleToScreen(Rectangle bounds)
{
return TreeView?.RectangleToScreen(bounds) ?? Rectangle.Empty;
}
}
}
29 changes: 23 additions & 6 deletions src/System.Windows.Forms/src/System/Windows/Forms/TreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable disable

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
Expand All @@ -24,7 +25,7 @@ namespace System.Windows.Forms
[TypeConverterAttribute(typeof(TreeNodeConverter))]
[Serializable] // This class participates in resx serialization.
[DefaultProperty(nameof(Text))]
public class TreeNode : MarshalByRefObject, ICloneable, ISerializable
public partial class TreeNode : MarshalByRefObject, ICloneable, ISerializable
{
internal const int SHIFTVAL = 12;
private const TVIS CHECKED = (TVIS)(2 << SHIFTVAL);
Expand Down Expand Up @@ -1796,6 +1797,7 @@ public void ExpandAll()
children[i].ExpandAll();
}
}

/// <summary>
/// Locate this tree node's containing tree view control by scanning
/// up to the virtual root, whose treeView pointer we know to be
Expand All @@ -1812,6 +1814,22 @@ internal TreeView FindTreeView()
return node.treeView;
}

internal List<TreeNode> GetSelfAndChildNodes()
{
List<TreeNode> nodes = new List<TreeNode>() { this };
AggregateChildNodesToList(this);
return nodes;

void AggregateChildNodesToList(TreeNode parentNode)
{
foreach (TreeNode child in parentNode.Nodes)
{
nodes.Add(child);
AggregateChildNodesToList(child);
}
}
}

/// <summary>
/// Helper function for getFullPath().
/// </summary>
Expand Down Expand Up @@ -2035,11 +2053,11 @@ internal void Remove(bool notify)

// unlink our children
//

for (int i = 0; i < childCount; i++)
{
children[i].Remove(false);
}

// children = null;
// unlink ourself
if (notify && parent != null)
Expand All @@ -2063,6 +2081,8 @@ internal void Remove(bool notify)
return;
}

KeyboardToolTipStateMachine.Instance.Unhook(this, tv.KeyboardToolTip);

if (handle != IntPtr.Zero)
{
if (notify && tv.IsHandleCreated)
Expand Down Expand Up @@ -2287,9 +2307,6 @@ internal unsafe void UpdateImage()
/// <summary>
/// ISerializable private implementation
/// </summary>
void ISerializable.GetObjectData(SerializationInfo si, StreamingContext context)
{
Serialize(si, context);
}
void ISerializable.GetObjectData(SerializationInfo si, StreamingContext context) => Serialize(si, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ private int AddInternal(TreeNode node, int delta)

// If the TreeView is sorted, index is ignored
TreeView tv = owner.TreeView;

if (tv is not null)
{
foreach (TreeNode treeNode in node.GetSelfAndChildNodes())
{
KeyboardToolTipStateMachine.Instance.Hook(treeNode, tv.KeyboardToolTip);
}
}

if (tv != null && tv.Sorted)
{
return owner.AddSorted(node);
Expand Down Expand Up @@ -516,6 +525,15 @@ public virtual void Insert(int index, TreeNode node)

// If the TreeView is sorted, index is ignored
TreeView tv = owner.TreeView;

if (tv is not null)
{
foreach (TreeNode treeNode in node.GetSelfAndChildNodes())
{
KeyboardToolTipStateMachine.Instance.Hook(treeNode, tv.KeyboardToolTip);
}
}

if (tv != null && tv.Sorted)
{
owner.AddSorted(node);
Expand Down
64 changes: 63 additions & 1 deletion src/System.Windows.Forms/src/System/Windows/Forms/TreeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable disable

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
Expand Down Expand Up @@ -863,6 +864,8 @@ public int ItemHeight
}
}

internal ToolTip KeyboardToolTip { get; } = new ToolTip();

/// <summary>
/// The LabelEdit property determines if the label text
/// of nodes in the tree view is editable.
Expand Down Expand Up @@ -1639,7 +1642,9 @@ protected override void Dispose(bool disposing)
}

// Dispose unmanaged resources.
UnhookNodes();
_toolTipBuffer.Dispose();
KeyboardToolTip.Dispose();

base.Dispose(disposing);
}
Expand Down Expand Up @@ -1806,6 +1811,22 @@ private void ImageListChangedHandle(object sender, EventArgs e)
}
}

private void NotifyAboutGotFocus(TreeNode treeNode)
{
if (treeNode is not null)
{
KeyboardToolTipStateMachine.Instance.NotifyAboutGotFocus(treeNode);
}
}

private void NotifyAboutLostFocus(TreeNode treeNode)
{
if (treeNode is not null)
{
KeyboardToolTipStateMachine.Instance.NotifyAboutLostFocus(treeNode);
}
}

private void StateImageListRecreateHandle(object sender, EventArgs e)
{
if (IsHandleCreated)
Expand Down Expand Up @@ -2130,6 +2151,7 @@ protected override void OnMouseHover(EventArgs e)
{
OnNodeMouseHover(new TreeNodeMouseHoverEventArgs(tn));
prevHoveredNode = tn;
NotifyAboutLostFocus(SelectedNode);
}
}

Expand Down Expand Up @@ -2508,6 +2530,7 @@ private unsafe IntPtr TvnSelecting(NMTREEVIEW* nmtv)
{
case TVC.BYKEYBOARD:
action = TreeViewAction.ByKeyboard;
NotifyAboutLostFocus(SelectedNode);
break;
case TVC.BYMOUSE:
action = TreeViewAction.ByMouse;
Expand All @@ -2530,18 +2553,20 @@ private unsafe void TvnSelected(NMTREEVIEW* nmtv)

if (nmtv->itemNew.hItem != IntPtr.Zero)
{
TreeNode node = NodeFromHandle(nmtv->itemNew.hItem);
TreeViewAction action = TreeViewAction.Unknown;
switch (nmtv->action)
{
case TVC.BYKEYBOARD:
action = TreeViewAction.ByKeyboard;
NotifyAboutGotFocus(node);
break;
case TVC.BYMOUSE:
action = TreeViewAction.ByMouse;
break;
}

OnAfterSelect(new TreeViewEventArgs(NodeFromHandle(nmtv->itemNew.hItem), action));
OnAfterSelect(new TreeViewEventArgs(node, action));
}

// TreeView doesn't properly revert back to the unselected image
Expand Down Expand Up @@ -2862,6 +2887,20 @@ protected OwnerDrawPropertyBag GetItemRenderStyles(TreeNode node, int state)
return retval;
}

internal unsafe override ComCtl32.ToolInfoWrapper<Control> GetToolInfoWrapper(TTF flags, string caption, ToolTip tooltip)
{
// The "ShowNodeToolTips" flag is required so that when the user hovers over the TreeNode,
// their own tooltip is displayed, not the TreeView tooltip.
// The second condition is necessary for the correct display of the keyboard tooltip,
// since the logic of the external tooltip blocks its display
bool isExternalTooltip = ShowNodeToolTips && tooltip != KeyboardToolTip;
var wrapper = new ComCtl32.ToolInfoWrapper<Control>(this, flags, isExternalTooltip ? null : caption);
if (isExternalTooltip)
wrapper.Info.lpszText = (char*)(-1);

return wrapper;
}

private unsafe bool WmShowToolTip(ref Message m)
{
User32.NMHDR* nmhdr = (User32.NMHDR*)m.LParam;
Expand Down Expand Up @@ -3034,6 +3073,18 @@ private unsafe void WmNotify(ref Message m)
}
}

protected override void OnGotFocus(EventArgs e)
{
base.OnGotFocus(e);
NotifyAboutGotFocus(SelectedNode);
}

protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
NotifyAboutLostFocus(SelectedNode);
}

/// <summary>
/// Shows the context menu for the Treenode.
/// </summary>
Expand All @@ -3059,6 +3110,17 @@ private void ContextMenuStripClosing(object sender, ToolStripDropDownClosingEven
User32.SendMessageW(this, (User32.WM)TVM.SELECTITEM, (IntPtr)TVGN.DROPHILITE);
}

private void UnhookNodes()
{
foreach (TreeNode rootNode in Nodes)
{
foreach (TreeNode node in rootNode.GetSelfAndChildNodes())
{
KeyboardToolTipStateMachine.Instance.Unhook(node, KeyboardToolTip);
}
}
}

private void WmPrint(ref Message m)
{
base.WndProc(ref m);
Expand Down
Loading

0 comments on commit 77dcb4f

Please sign in to comment.