mac: Support for pan, zoom and rotate trackpad gestures (#1093)
* mac: Support for pan, zoom and rotate trackpad gestures Currently SolveSpace is nearly unusable on a mac if you only have a buttonless trackpad and not a mouse, because there's no way to pan (ie right-click-drag) or rotate (ie middle-click-drag). You can zoom, but only by using two-finger-drag up and down, which ends up getting interpreted as a scrollwheel event. This change makes the app behave much more like any other mac app, by adding 2-finger-drag pan gesture support and pinch-gesture zooming, and 3D rotate using shift-2-finger-drag. I've also added support for the rotate two-finger trackpad gesture, which rotates directly around the screen Z axis (rather than in all 3 dimensions) which is actually something I've found myself wanting to do with the mouse but afaik there's no equivalent way of achieving that. While I was there, I fixed a bugette in convertMouseEvent which was incorrectly translating the NSEvent coordinates, and then fixing up the fact that the sign of the y-coordinate was wrong as a result. Using the convertPoint API correctly means that fixup is not required because convertPoint handles it for you. * Don't do trackpad gestures on anything except the toplevel window * mac: Fix non-functional scrollbar on text window Which has not worked quite right since the last major refactor. * Don't pass right-button drags to the toolbar This improves the behaviour of trackpad pan/rotate on mac which uses simulated right-button events. * Don't pass cmd/ctrl modifier through on trackpad pan/rotate MouseEventspull/1101/head
parent
31a709e2c8
commit
e1b0784b31
|
@ -103,7 +103,10 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown,
|
|||
shiftDown = !shiftDown;
|
||||
}
|
||||
|
||||
if(SS.showToolbar) {
|
||||
// Not passing right-button and middle-button drags to the toolbar avoids
|
||||
// some cosmetic issues with trackpad pans/rotates implemented with
|
||||
// simulated right-button drag events causing spurious hover events.
|
||||
if(SS.showToolbar && !middleDown) {
|
||||
if(ToolbarMouseMoved((int)x, (int)y)) {
|
||||
hover.Clear();
|
||||
return;
|
||||
|
|
|
@ -359,18 +359,25 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
- (void)didEdit:(NSString *)text;
|
||||
|
||||
@property double scrollerMin;
|
||||
@property double scrollerMax;
|
||||
@property double scrollerSize;
|
||||
@property double pageSize;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SSView
|
||||
{
|
||||
NSTrackingArea *trackingArea;
|
||||
NSTextField *editor;
|
||||
double magnificationGestureCurrentZ;
|
||||
double rotationGestureCurrent;
|
||||
Point2d trackpadPositionShift;
|
||||
bool inTrackpadScrollGesture;
|
||||
Platform::Window::Kind kind;
|
||||
}
|
||||
|
||||
@synthesize acceptsFirstResponder;
|
||||
|
||||
- (id)initWithFrame:(NSRect)frameRect {
|
||||
- (id)initWithKind:(Platform::Window::Kind)aKind {
|
||||
NSOpenGLPixelFormatAttribute attrs[] = {
|
||||
NSOpenGLPFADoubleBuffer,
|
||||
NSOpenGLPFAColorSize, 24,
|
||||
|
@ -378,7 +385,7 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
0
|
||||
};
|
||||
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
|
||||
if(self = [super initWithFrame:frameRect pixelFormat:pixelFormat]) {
|
||||
if(self = [super initWithFrame:NSMakeRect(0, 0, 0, 0) pixelFormat:pixelFormat]) {
|
||||
self.wantsBestResolutionOpenGLSurface = YES;
|
||||
self.wantsLayer = YES;
|
||||
editor = [[NSTextField alloc] init];
|
||||
|
@ -388,6 +395,18 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
editor.bezeled = NO;
|
||||
editor.target = self;
|
||||
editor.action = @selector(didEdit:);
|
||||
|
||||
inTrackpadScrollGesture = false;
|
||||
kind = aKind;
|
||||
if(kind == Platform::Window::Kind::TOPLEVEL) {
|
||||
NSGestureRecognizer *mag = [[NSMagnificationGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(magnifyGesture:)];
|
||||
[self addGestureRecognizer:mag];
|
||||
|
||||
NSRotationGestureRecognizer* rot = [[NSRotationGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(rotateGesture:)];
|
||||
[self addGestureRecognizer:rot];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -428,9 +447,9 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent {
|
||||
Platform::MouseEvent event = {};
|
||||
|
||||
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:self];
|
||||
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:nil];
|
||||
event.x = nsPoint.x;
|
||||
event.y = self.bounds.size.height - nsPoint.y;
|
||||
event.y = nsPoint.y;
|
||||
|
||||
NSUInteger nsFlags = [nsEvent modifierFlags];
|
||||
if(nsFlags & NSEventModifierFlagShift) event.shiftDown = true;
|
||||
|
@ -554,15 +573,58 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
using Platform::MouseEvent;
|
||||
|
||||
MouseEvent event = [self convertMouseEvent:nsEvent];
|
||||
if(nsEvent.subtype == NSEventSubtypeTabletPoint && kind == Platform::Window::Kind::TOPLEVEL) {
|
||||
// This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via
|
||||
// NSPanGestureRecognizer which is how you might expect this to work... We complicate this
|
||||
// further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using
|
||||
// shift in the same way as right-mouse-button MouseEvent does (to converts a pan to a
|
||||
// rotate) so we get the rotate support for free. It's a bit ugly having to fake mouse
|
||||
// events and track the deviation from the actual mouse cursor with trackpadPositionShift,
|
||||
// but in lieu of an event API that allows us to request a rotate/pan with relative
|
||||
// coordinates, it's the best we can do.
|
||||
event.button = MouseEvent::Button::RIGHT;
|
||||
// Make sure control (actually cmd) isn't passed through, ctrl-right-click-drag has special
|
||||
// meaning as rotate which we don't want to inadvertently trigger.
|
||||
event.controlDown = false;
|
||||
if(nsEvent.scrollingDeltaX == 0 && nsEvent.scrollingDeltaY == 0) {
|
||||
// Cocoa represents the point where the user lifts their fingers off (and any inertial
|
||||
// scrolling has finished) by an event with scrollingDeltaX and scrollingDeltaY both 0.
|
||||
// Sometimes you also get a zero scroll at the start of a two-finger-rotate (probably
|
||||
// reflecting the internal implementation of that being a cancelled possible pan
|
||||
// gesture), which is why this conditional is structured the way it is.
|
||||
if(inTrackpadScrollGesture) {
|
||||
event.x += trackpadPositionShift.x;
|
||||
event.y += trackpadPositionShift.y;
|
||||
event.type = MouseEvent::Type::RELEASE;
|
||||
receiver->onMouseEvent(event);
|
||||
inTrackpadScrollGesture = false;
|
||||
trackpadPositionShift = Point2d::From(0, 0);
|
||||
}
|
||||
return;
|
||||
} else if(!inTrackpadScrollGesture) {
|
||||
inTrackpadScrollGesture = true;
|
||||
trackpadPositionShift = Point2d::From(0, 0);
|
||||
event.type = MouseEvent::Type::PRESS;
|
||||
receiver->onMouseEvent(event);
|
||||
// And drop through
|
||||
}
|
||||
|
||||
trackpadPositionShift.x += nsEvent.scrollingDeltaX;
|
||||
trackpadPositionShift.y += nsEvent.scrollingDeltaY;
|
||||
event.type = MouseEvent::Type::MOTION;
|
||||
event.x += trackpadPositionShift.x;
|
||||
event.y += trackpadPositionShift.y;
|
||||
receiver->onMouseEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
event.type = MouseEvent::Type::SCROLL_VERT;
|
||||
|
||||
bool isPrecise = [nsEvent hasPreciseScrollingDeltas];
|
||||
event.scrollDelta = [nsEvent scrollingDeltaY] / (isPrecise ? 50 : 5);
|
||||
|
||||
if(receiver->onMouseEvent) {
|
||||
receiver->onMouseEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseExited:(NSEvent *)nsEvent {
|
||||
using Platform::MouseEvent;
|
||||
|
@ -639,6 +701,50 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
[super keyUp:nsEvent];
|
||||
}
|
||||
|
||||
- (void)magnifyGesture:(NSMagnificationGestureRecognizer *)gesture {
|
||||
// The onSixDofEvent API doesn't allow us to specify the scaling's origin, so for expediency
|
||||
// we fake out a scrollwheel MouseEvent with a suitably-scaled scrollDelta with a bit of
|
||||
// absolute-to-relative positioning conversion tracked using magnificationGestureCurrentZ.
|
||||
|
||||
if(gesture.state == NSGestureRecognizerStateBegan) {
|
||||
magnificationGestureCurrentZ = 0.0;
|
||||
}
|
||||
|
||||
// Magic number to make gesture.magnification align roughly with what scrollDelta expects
|
||||
constexpr double kScale = 10.0;
|
||||
double z = ((double)gesture.magnification * kScale);
|
||||
double zdelta = z - magnificationGestureCurrentZ;
|
||||
magnificationGestureCurrentZ = z;
|
||||
|
||||
using Platform::MouseEvent;
|
||||
MouseEvent event = {};
|
||||
event.type = MouseEvent::Type::SCROLL_VERT;
|
||||
NSPoint nsPoint = [gesture locationInView:self];
|
||||
event.x = nsPoint.x;
|
||||
event.y = nsPoint.y;
|
||||
event.scrollDelta = zdelta;
|
||||
if(receiver->onMouseEvent) {
|
||||
receiver->onMouseEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rotateGesture:(NSRotationGestureRecognizer *)gesture {
|
||||
if(gesture.state == NSGestureRecognizerStateBegan) {
|
||||
rotationGestureCurrent = 0.0;
|
||||
}
|
||||
double rotation = gesture.rotation;
|
||||
double rotationDelta = rotation - rotationGestureCurrent;
|
||||
rotationGestureCurrent = rotation;
|
||||
|
||||
using Platform::SixDofEvent;
|
||||
SixDofEvent event = {};
|
||||
event.type = SixDofEvent::Type::MOTION;
|
||||
event.rotationZ = rotationDelta;
|
||||
if(receiver->onSixDofEvent) {
|
||||
receiver->onSixDofEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
@synthesize editing;
|
||||
|
||||
- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight
|
||||
|
@ -699,11 +805,27 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
}
|
||||
|
||||
@synthesize scrollerMin;
|
||||
@synthesize scrollerMax;
|
||||
@synthesize scrollerSize;
|
||||
@synthesize pageSize;
|
||||
|
||||
- (void)didScroll:(NSScroller *)sender {
|
||||
double pos;
|
||||
switch(sender.hitPart) {
|
||||
case NSScrollerKnob:
|
||||
case NSScrollerKnobSlot:
|
||||
pos = receiver->GetScrollbarPosition();
|
||||
break;
|
||||
case NSScrollerDecrementPage:
|
||||
pos = receiver->GetScrollbarPosition() - pageSize;
|
||||
break;
|
||||
case NSScrollerIncrementPage:
|
||||
pos = receiver->GetScrollbarPosition() + pageSize;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if(receiver->onScrollbarAdjusted) {
|
||||
double pos = scrollerMin + [sender doubleValue] * (scrollerMax - scrollerMin);
|
||||
receiver->onScrollbarAdjusted(pos);
|
||||
}
|
||||
}
|
||||
|
@ -770,7 +892,7 @@ public:
|
|||
NSString *nsToolTip;
|
||||
|
||||
WindowImplCocoa(Window::Kind kind, std::shared_ptr<WindowImplCocoa> parentWindow) {
|
||||
ssView = [[SSView alloc] init];
|
||||
ssView = [[SSView alloc] initWithKind:kind];
|
||||
ssView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
ssView.receiver = this;
|
||||
|
||||
|
@ -963,21 +1085,22 @@ public:
|
|||
|
||||
void ConfigureScrollbar(double min, double max, double pageSize) override {
|
||||
ssView.scrollerMin = min;
|
||||
ssView.scrollerMax = max - pageSize;
|
||||
[nsScroller setKnobProportion:(pageSize / (ssView.scrollerMax - ssView.scrollerMin))];
|
||||
ssView.scrollerSize = max + 1 - min;
|
||||
ssView.pageSize = pageSize;
|
||||
nsScroller.knobProportion = pageSize / ssView.scrollerSize;
|
||||
nsScroller.hidden = pageSize >= ssView.scrollerSize;
|
||||
}
|
||||
|
||||
double GetScrollbarPosition() override {
|
||||
// Platform::Window scrollbar positions are in the range [min, max+1 - pageSize] inclusive,
|
||||
// and Cocoa scrollbars are from 0.0 to 1.0 inclusive, so we have to apply some scaling and
|
||||
// transforming. (scrollerSize is max+1-min, see ConfigureScrollbar above)
|
||||
return ssView.scrollerMin +
|
||||
[nsScroller doubleValue] * (ssView.scrollerMax - ssView.scrollerMin);
|
||||
nsScroller.doubleValue * (ssView.scrollerSize - ssView.pageSize);
|
||||
}
|
||||
|
||||
void SetScrollbarPosition(double pos) override {
|
||||
if(pos > ssView.scrollerMax)
|
||||
pos = ssView.scrollerMax;
|
||||
if(GetScrollbarPosition() == pos)
|
||||
return;
|
||||
[nsScroller setDoubleValue:(pos / (ssView.scrollerMax - ssView.scrollerMin))];
|
||||
nsScroller.doubleValue = (pos - ssView.scrollerMin) / ( ssView.scrollerSize - ssView.pageSize);
|
||||
}
|
||||
|
||||
void Invalidate() override {
|
||||
|
|
Loading…
Reference in New Issue