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

New ToolTipClosing, ToolTipOpening attached events and ToolTip.Opened, ToolTip.Closed #15493

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

maxkatz6
Copy link
Member

@maxkatz6 maxkatz6 commented Apr 24, 2024

What does the pull request do?

public class ToolTip
{
+     public static readonly RoutedEvent ToolTipOpeningEvent;
+     public static readonly RoutedEvent ToolTipClosingEvent;
+     public static void AddToolTipOpeningHandler(Control element, EventHandler<RoutedEventArgs> handler);
+     public static void RemoveToolTipOpeningHandler(Control element, EventHandler<RoutedEventArgs> handler);
+     public static void AddToolTipClosingHandler(Control element, EventHandler<RoutedEventArgs> handler);
+     public static void RemoveToolTipClosingHandler(Control element, EventHandler<RoutedEventArgs> handler);
}

Behavior is ported from WPF, but implementation is different, just like our ToolTip API is already quite different. Most of this decisions were made for consistency with the rest of the framework.

Common:

  1. When ToolTipOpening is raised, it's possible to replace TipProperty, which will cause opening of another tip.
  2. It's also possible to set ToolTipOpening Handled=true, which will essentially cancel tooltip opening.
  3. It's not possible to cancel ToolTipClosing in any way.

Different:

  1. Instead of adding attached events to the ToolTipService, I added them to ToolTip class. Following consistency with other attached ToolTip APIs.
  2. Same about adding new members to the Control class - we never added anything ToolTip (or ContextMenu) related to the Control type itself - everything is attached.
  3. Opened/Closed events are direct-routed in WPF, but CLR events in this PR. Following the same behavior with ContextMenu and Flyout events.

To raise Opening/Closing events, I used IsOpen.Coerse callback, which is also handy to cancel tooltip opening. In WPF these events were only usable via ToolTipService, and handled there. In general, in Avalonia tooltip is completely usable without ToolTipService, so these events should as well.

Checklist

Fixed issues

Fixes #15232

@maxkatz6 maxkatz6 added api API addition backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch labels Apr 24, 2024
@maxkatz6 maxkatz6 requested a review from grokys April 24, 2024 04:13
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0047624-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@BAndysc
Copy link
Contributor

BAndysc commented Apr 24, 2024

In case of custom drawn controls, tooltip content may depend on the position of the cursor thus it would be very useful to have a location of the pointer in ToolTipOpeningEvent that is causing the tooltip to show up if it is possible

@MrJul
Copy link
Member

MrJul commented Apr 24, 2024

3. It's not possible to cancel ToolTipClosing in any way.

Is it a technical limitation? I can see uses in preventing tooltips from closing.
(Edit: for reference ContextMenu and Flyout have preventable Closing)

@maxkatz6
Copy link
Member Author

@MrJul I am just following WPF behavior here. Technically, it can be done.

@BAndysc would be a bit tricky to pass position from ToolTipService to ToolTip.

@MrJul
Copy link
Member

MrJul commented Apr 24, 2024

Should we unify the API between all popup-like controls here?

Having all necessary events (Opening, Opened, Closing, Closed) on Popup and re-trigger them on ContextMenu, ToolTip and Flyout. Currently it feels a bit random, and the signature sometimes differ between controls (e.g. CancelEventArgs vs standard event args).

if (isOpen)
{
var args = new RoutedEventArgs(ToolTipOpeningEvent);
control.RaiseEvent(args);
Copy link
Member

Choose a reason for hiding this comment

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

I'll let @grokys comment on this, but I'm a bit worried of having such side effects in coercion methods, potentially leading to reentrancy or other hard to debug problems. Coercion methods are called in the middle of setting an EffectiveValue, and I don't think this code currently handle reentrancy. (For example, PropertyChanged isn't called until all the internal effective value bookkeeping is done to avoid issues.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, didn't think about reentrancy. It's technically possible to change IsOpen value inside.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's also possible for the events to be raised when the value isn't changing. Aside from any internal coercion calls, the user can do this at any time:

myControl.CoerceValue(ToolTip.IsOpenProperty);

ToolTip.IsOpenChanged is the right place for this code. It's a bit weird that IsOpen will be true when ToolTipOpening is raised, but the event name makes it very clear what's going on.

@robloo
Copy link
Contributor

robloo commented Apr 24, 2024

Should we unify the API between all popup-like controls here?

Having all necessary events (Opening, Opened, Closing, Closed) on Popup and re-trigger them on ContextMenu, ToolTip and Flyout. Currently it feels a bit random, and the signature sometimes differ between controls (e.g. CancelEventArgs vs standard event args).

I'm a big fan of unifying APIs everywhere it's possible. This is a great place to do it IMO.

Also note that Expander is related to this discussion and while the naming is different (Expanding/Collapsing/etc.) usage and API patterns should be the same as the rest.

@billhenn
Copy link
Contributor

@maxkatz6 The update works well. I was able to dynamically update a tooltip before it displayed.

I agree that having a consistent API for the Opening, Opened, Closing, and Closed events in various places would be ideal.

I'm also usually a fan of using something like e.Cancel to prevent a default behavior. e.Handled is technically there to mean stop routing an event further. The way WPF uses it for ToolTipOpening events is sort of a gray area, since they hijack e.Handled to say whether to cancel the default functionality. One could probably argue either way on that usage.

@robloo
Copy link
Contributor

robloo commented Apr 24, 2024

@billhenn @maxkatz6

I'm also usually a fan of using something like e.Cancel to prevent a default behavior. e.Handled is technically there to mean stop routing an event further. The way WPF uses it for ToolTipOpening events is sort of a gray area, since they hijack e.Handled to say whether to cancel the default functionality. One could probably argue either way on that usage.

Note that this exact topic was discussed for Expander here: #9979 (comment). The agreement was to add CancelRoutedEventArgs to go along with the existing CancelEventArgs establishing Avalonia's convention. We differ here from WPF but it's better for the same reasons you state. All controls should be using this convention -- tooltip as well.

Just keep in mind Avalonia has these cancel event args already and 3rd party libraries should ideally use them as well.

/// ToolTipOpening will not be raised if the value of ToolTip is null or otherwise unset. Do not deliberately set ToolTip to null while a tooltip is open or opening; this will not have the effect of closing the tooltip, and will instead create an undesirable visual artifact in the UI.
/// </remarks>
public static readonly RoutedEvent ToolTipOpeningEvent =
RoutedEvent.Register<ToolTip, RoutedEventArgs>("ToolTipOpening", RoutingStrategies.Direct);
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed new events here should follow existing conventions and use CancelRoutedEventArgs. This follows Expander which itself was based on what Flyout did (although flyout isn't API symmetric and doesn't allow cancelling an open).

@jmacato
Copy link
Member

jmacato commented Apr 25, 2024

@cla-avalonia recheck

@cla-avalonia
Copy link
Collaborator

cla-avalonia commented Apr 25, 2024

  • All contributors have signed the CLA.

@maxkatz6
Copy link
Member Author

@cla-avalonia agree

@maxkatz6
Copy link
Member Author

Updated PR:

  1. Reimplemented attached events to not rely on Coerce callback
  2. Changed Opening args type from RoutedEventArgs to CancelRoutedEventArgs
  3. Removed Opened and Closed events. These weren't part of the initial issue, and can be re-added in the future. It's likely involve breaking changes in ContextMenu or Flyout controls, as these have different events. Also, Popup itself wasn't really intended to be cancellable.

Note, Closing event is still not cancellable. If necessary, it can be implemented later.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0048346-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@billhenn
Copy link
Contributor

@maxkatz6 Thanks, I tried the PR build and it does what we needed for the tooltip opening notification.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0048447-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api API addition backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Control.ToolTipOpening/ToolTipClosing events
9 participants