Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@moffatman
Copy link
Contributor

@moffatman moffatman commented Aug 26, 2021

On iPadOS, if you are using a flutter application with a mouse or trackpad connected, but then disconnect that device, the pointer will stay in the framework at its previous position. So any Widget which interacts on PointerEnter/PointerExit will behave strangely. with no way to resolve it.
There is a way to query whether the pointer should be shown any more, but no way to get a callback from the system when this happens. So I perform the check when the user touches the screen, this should still provide a much better experience than before.

The test doesn't pass on the most recent Xcode 13 betas, the hover selector is broken in the simulator. I filed radar FB9427343 so hopefully that will be addressed. But CI is not running the iPadGestureTests yet anyways.

Fixes flutter/flutter#89878

Copy link
Contributor

@chinmaygarde chinmaygarde left a comment

Choose a reason for hiding this comment

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

@gaaclarke
Copy link
Member

Can you file an issue and link it in the description please? That helps other people that run into this problem discover it.

What do you think about an approach that would fire an NSTimer to poll for the presence of the device every 500ms one a device is added and remove it when it's no longer present, instead of relying on the next touch event to clear it out?

@moffatman
Copy link
Contributor Author

What do you think about an approach that would fire an NSTimer to poll for the presence of the device every 500ms one a device is added and remove it when it's no longer present, instead of relying on the next touch event to clear it out?

I thought about it, and initially didn't like polling approach. And also, iOS removes the cursor/focus environment when the user hasn't moved the mouse for only a few seconds (I measured it at ~3.5 seconds). I suppose it's iOS policy that the cursor should be removed when inactive, so maybe it should be followed. If the user was actively hovering something when the cursor went inactive, though, it might be unexpected. They would see the cursor fade out, then ~0-500ms later, the hover stop.

In one of my Flutter apps, I have some content that appears on hover, sometimes I leave the mouse inactive while I read the content. If the Flutter cursor was also removed, it would be annoying, but I suppose it's rare to use that desktop UI paradigm on the iPad. Mostly on iPad hover is just used to do the fancy cursor effects.

@gaaclarke
Copy link
Member

Couldn't we mimic that behavior though, right? Anytime there is cursor activity we reset the timer that is set for ~3.5 seconds. That way we are matching the behavior of UIKit apps, no?

@moffatman
Copy link
Contributor Author

It's a good idea, I implemented it but I realized that the UIFocusSystemCheck doesn't care about whether the cursor is active, it's just if the mouse is connected or not. I think I was confused with some other APIs I was trying out to check the pointer status, there isn't a great official one. But this means I can implement your original suggestion, check every 500ms if the mouse is plugged in.

@chinmaygarde
Copy link
Contributor

Just a drive by comment about "but then disconnect that device, the pointer will stay in the framework at its previous position". Isn't it the case that devices when disconnected broadcast a cancelled pointer event? The framework should be able to detect that instead of there being a need to poll for the active pointers.

@moffatman
Copy link
Contributor Author

@chinmaygarde
The hover pointer is not like other pointers, Flutter's use case is not well-supported. We only know its position by intercepting UIKit's request for a rectangle to apply cursor effects. And we can only tell if it's no longer connected by polling this side channel.
@gaaclarke
I would say this is ready for review but I don't know if it's testable, I have found the iPad simulator to be broken for testing pointer interactions since ~Xcode 13 beta 2.

@zanderso zanderso requested a review from gaaclarke October 7, 2021 20:15
@chinmaygarde
Copy link
Contributor

If @gaaclarke is busy, I can take a stab at reviewing this. It looks like there is consensus on this approach being the right way forward.

Copy link
Contributor

@chinmaygarde chinmaygarde left a comment

Choose a reason for hiding this comment

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

The guide for tests discourages timeouts but I am not able to provide better guidance on how to test this. LGTM.

@moffatman
Copy link
Contributor Author

I found that this is currently not testable, the pointerInteraction here does not get triggered by synthetic hover events in recent versions of Xcode (it did at one point work during the beta cycle for Xcode 13). I discovered that when using UIHoverGestureRecognizer, it does get triggered properly. If we use that class instead of PointerInteraction we will also get a state-change when the pointer is disconnected, so it's overall a better solution for this issue in general. I will update this PR with that change, let's not merge this.

pointer_data.change = flutter::PointerData::Change::kRemove;
break;
default:
FML_LOG(ERROR) << "Unhandled hover phase: " << _hoverGestureRecognizer.get().state;
Copy link
Contributor

Choose a reason for hiding this comment

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

This log is not user actionable. Let's remove it. Also, flutter::PointerData::Change::kCancel seems more apt here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

kCancel is meant for when the pointer is down, since this pointer is never down it will cause failed assertions during processing.
Since the remaining unhandled and unexpected states are all a kind of semi-recognized I thought that kHover was the closest choice. And it's the only one that will not cause a fatal double add or double remove.

@jmagman
Copy link
Member

jmagman commented Nov 15, 2021

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

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

The view controller code looks good. Had some nits about the test, and suggestions for making what it is expecting clearer.
I don't have a bluetooth mouse to test this with at the moment.

NSMutableDictionary* messages = [NSMutableDictionary dictionary];
for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
NSString* rawMessage = element.value;
NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
Copy link
Member

Choose a reason for hiding this comment

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

This section is hard to understand, what is it doing, exactly? Does -componentsSeparatedByString make it easier to read?

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 just need the first component, I don't want to split the whole string by commas since there are more commas later.

@"PointerChange.up event did not occur for a right-click");
int rightClickAddedSeqNo, rightClickDownSeqNo, rightClickUpSeqNo;
if ([messages objectForKey:@"PointerChange.add,device=2,buttons=0"] == nil) {
// Sometimes the tap pointer has the same device as the right-click (the UITouch is reused)
Copy link
Member

Choose a reason for hiding this comment

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

This test generally may be easier to read as a series of expectations, with ordering either enforced or not depending on what you're checking.

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 observed a lot of different permutations of event order, even though this is somewhat convoluted I don't think the alternative is better.

@CaseyHillers CaseyHillers changed the base branch from master to main November 15, 2021 18:16
@moffatman moffatman requested a review from jmagman November 17, 2021 17:13
@godofredoc godofredoc changed the base branch from master to main November 24, 2021 07:21
@gaaclarke
Copy link
Member

@jmagman friendly ping. I looked through your feedback and their response, this looks good to me now.

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

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

LGTM

[app.textFields[@"1,PointerChange.hover,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.hover event did not occur for a hover");
// The number of hover events fired is not always the same
NSInteger lastHoverSequenceNumber = -1;
Copy link
Member

Choose a reason for hiding this comment

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

Initializing to NSNotFound and checking (lastHoverSequenceNumber == NSNotFound) || (messageSequenceNumber > lastHoverSequenceNumber) is probably more idiomatic than -1, but this is fine.

@jmagman jmagman added waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. and removed waiting for customer response labels Nov 29, 2021
@fluttergithubbot fluttergithubbot merged commit 0236853 into flutter:main Nov 29, 2021
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Nov 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

cla: yes platform-ios waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The iPad mouse cursor should be removed when the mouse is disconnected.

5 participants