dotnet / dotnet/winforms

Need ability to not always release mouse capture at end of `WmMouseUp` for multi-button gestures

Open
#13,126 1 comment 0 reactions 1 assignee Claimed by @JeremyKuhne View on GitHub
api-suggestion
Dominant language
C#
Stars
4.9k
Forks
1.1k
Avg merge
20h 23m
Merged PRs (30d)
103

Description

### Background and motivation

Over in `Control.WmMouseUp()`, mouse capture is always relinquished in a `finally` block at the end:

https://github.com/dotnet/winforms/blob/8158b01dc6a17adf93b3b5b4e95110400681991e/src/System.Windows.Forms/System/Windows/Forms/Control.cs#L11596
```cs
finally
{
// Always reset the States.DoubleClickFired in UP. Since we get UP - DOWN - DBLCLK - UP sequence
// The flag is set in L_BUTTONDBLCLK in the controls WndProc().
SetState(States.DoubleClickFired, false);
SetState(States.MousePressed, false);
SetState(States.ValidationCancelled, false);

// Capture is reset while exiting MouseUp.
Capture = false;
}
```

I have a very old bug in Paint.NET that is a result of this, and which I can't work around unless I want to completely handle `WM_MOUSEUP` myself and redo all of `Control.WmMouseUp()`'s logic. Which I don't think is even possible, as it's doing internal/private bookkeeping and whatnot.

The bug is a little weird but is something that a power user would trip over regularly:

1. First, install Paint.NET or download the portable version, and then launch it. https://github.com/paintdotnet/release
2. Make sure you're zoomed in enough so that the canvas extends beyond the bounds of the viewport (this isn't critical it just makes it easier). You can press Ctrl + + several times to do this.
3. Also make sure that the floating windows (Tools, History, Layers, Colors) are visible (not hidden). This is their default state.
4. Start drawing a selection with the Rectangle Select tool (it's the first tool in the Tools window). This is done by clicking and dragging with the left mouse button.
5. Move the mouse a bit so that you have a selection of pretty much any size. _Don't_ release the left mouse button yet.
6. While still holding down the left mouse button, also click and hold down the right mouse button. This invokes the "move selection while drawing it" state
7. Move the mouse so that it ends up over a floating window. (at this point you still have _both_ the left and right mouse buttons pressed)

**Expected:** The canvas maintains mouse capture while the mouse is over the floating window. (Conceptually you can really think of the mouse as being "under" the floating window due to capture)

**Actual:** The canvas loses focus and selection drawing immediately stops. This is because the floating windows will take focus when the mouse is over them _and_ the mouse is not captured elsewhere.

---

My proposal is to have some way to configure this, perhaps by making it so that `Capture` is not set to `false` unless all mouse buttons are arriving at the released state. This is the only scenario I have for this so I'm not sure if I have a good idea of a general purpose API. I am fine with a very narrowly tailored way of accomplishing this, whether with a property or a virtual method of some kind.

### API Proposal

```cs
public class Control
{
// This is a terrible name please come up with something better
bool OnlyReleaseCaptureWhenAllMouseButtonsAreReleased
{
get;
set;
} = false;
}
```

Hand-wavey implementation:
```cs
public class Control
{
...
///
/// Handles the WM_MOUSEUP message.
///
private void WmMouseUp(ref Message m, MouseButtons button, int clicks)
{
...
finally
{
// **** I'm not sure if any of these needs to be adjusted
// Always reset the States.DoubleClickFired in UP. Since we get UP - DOWN - DBLCLK - UP sequence
// The flag is set in L_BUTTONDBLCLK in the controls WndProc().
SetState(States.DoubleClickFired, false);
SetState(States.MousePressed, false);
SetState(States.ValidationCancelled, false);

// **** Need a new if() statement here
if (!OnlyReleaseCaptureWhenAllMouseButtonsAreReleased || AllMouseButtonsAreReleased())
{
Capture = false;
}
}
}
}
```

### API Usage

In my code I would do:

```cs
internal sealed class CanvasControl : ...
{
public CanvasControl()
{
...
this.OnlyReleaseCaptureWhenAllMouseButtonsAreReleased = true;
...
}
}
```

### Alternative Designs

Maybe with a virtual method to permit full customization of what happens.

```cs
...
///
/// Handles the WM_MOUSEUP message.
///
private void WmMouseUp(ref Message m, MouseButtons button, int clicks)
{
...
finally
{
...
OnEndMouseUp();
}
}

protected virtual void OnEndMouseUp()
{
Capture = false;
}
}
```

### Risks

Because `WmMouseUp` intersects with many facets of mouse behavior including clicking and double-clicking, care should be taken to not break things in order to enable this scenario. There may be other locations in `Control` where `Capture` would need to be set to `false` if, for instance, a `WM_MOUSEUP` was not received due to some other state transition.

### Will this feature affect UI controls?

Yes, this would likely need to be surfaced in the designer just like most other mutable properties.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.