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;
|
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)) {
|
if(ToolbarMouseMoved((int)x, (int)y)) {
|
||||||
hover.Clear();
|
hover.Clear();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -359,18 +359,25 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
- (void)didEdit:(NSString *)text;
|
- (void)didEdit:(NSString *)text;
|
||||||
|
|
||||||
@property double scrollerMin;
|
@property double scrollerMin;
|
||||||
@property double scrollerMax;
|
@property double scrollerSize;
|
||||||
|
@property double pageSize;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation SSView
|
@implementation SSView
|
||||||
{
|
{
|
||||||
NSTrackingArea *trackingArea;
|
NSTrackingArea *trackingArea;
|
||||||
NSTextField *editor;
|
NSTextField *editor;
|
||||||
|
double magnificationGestureCurrentZ;
|
||||||
|
double rotationGestureCurrent;
|
||||||
|
Point2d trackpadPositionShift;
|
||||||
|
bool inTrackpadScrollGesture;
|
||||||
|
Platform::Window::Kind kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize acceptsFirstResponder;
|
@synthesize acceptsFirstResponder;
|
||||||
|
|
||||||
- (id)initWithFrame:(NSRect)frameRect {
|
- (id)initWithKind:(Platform::Window::Kind)aKind {
|
||||||
NSOpenGLPixelFormatAttribute attrs[] = {
|
NSOpenGLPixelFormatAttribute attrs[] = {
|
||||||
NSOpenGLPFADoubleBuffer,
|
NSOpenGLPFADoubleBuffer,
|
||||||
NSOpenGLPFAColorSize, 24,
|
NSOpenGLPFAColorSize, 24,
|
||||||
|
@ -378,7 +385,7 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
|
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.wantsBestResolutionOpenGLSurface = YES;
|
||||||
self.wantsLayer = YES;
|
self.wantsLayer = YES;
|
||||||
editor = [[NSTextField alloc] init];
|
editor = [[NSTextField alloc] init];
|
||||||
|
@ -388,6 +395,18 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
editor.bezeled = NO;
|
editor.bezeled = NO;
|
||||||
editor.target = self;
|
editor.target = self;
|
||||||
editor.action = @selector(didEdit:);
|
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;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -428,9 +447,9 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent {
|
- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent {
|
||||||
Platform::MouseEvent event = {};
|
Platform::MouseEvent event = {};
|
||||||
|
|
||||||
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:self];
|
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:nil];
|
||||||
event.x = nsPoint.x;
|
event.x = nsPoint.x;
|
||||||
event.y = self.bounds.size.height - nsPoint.y;
|
event.y = nsPoint.y;
|
||||||
|
|
||||||
NSUInteger nsFlags = [nsEvent modifierFlags];
|
NSUInteger nsFlags = [nsEvent modifierFlags];
|
||||||
if(nsFlags & NSEventModifierFlagShift) event.shiftDown = true;
|
if(nsFlags & NSEventModifierFlagShift) event.shiftDown = true;
|
||||||
|
@ -554,14 +573,57 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
using Platform::MouseEvent;
|
using Platform::MouseEvent;
|
||||||
|
|
||||||
MouseEvent event = [self convertMouseEvent:nsEvent];
|
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;
|
event.type = MouseEvent::Type::SCROLL_VERT;
|
||||||
|
|
||||||
bool isPrecise = [nsEvent hasPreciseScrollingDeltas];
|
bool isPrecise = [nsEvent hasPreciseScrollingDeltas];
|
||||||
event.scrollDelta = [nsEvent scrollingDeltaY] / (isPrecise ? 50 : 5);
|
event.scrollDelta = [nsEvent scrollingDeltaY] / (isPrecise ? 50 : 5);
|
||||||
|
|
||||||
if(receiver->onMouseEvent) {
|
receiver->onMouseEvent(event);
|
||||||
receiver->onMouseEvent(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)mouseExited:(NSEvent *)nsEvent {
|
- (void)mouseExited:(NSEvent *)nsEvent {
|
||||||
|
@ -639,6 +701,50 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
[super keyUp:nsEvent];
|
[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;
|
@synthesize editing;
|
||||||
|
|
||||||
- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight
|
- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight
|
||||||
|
@ -699,11 +805,27 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize scrollerMin;
|
@synthesize scrollerMin;
|
||||||
@synthesize scrollerMax;
|
@synthesize scrollerSize;
|
||||||
|
@synthesize pageSize;
|
||||||
|
|
||||||
- (void)didScroll:(NSScroller *)sender {
|
- (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) {
|
if(receiver->onScrollbarAdjusted) {
|
||||||
double pos = scrollerMin + [sender doubleValue] * (scrollerMax - scrollerMin);
|
|
||||||
receiver->onScrollbarAdjusted(pos);
|
receiver->onScrollbarAdjusted(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -770,7 +892,7 @@ public:
|
||||||
NSString *nsToolTip;
|
NSString *nsToolTip;
|
||||||
|
|
||||||
WindowImplCocoa(Window::Kind kind, std::shared_ptr<WindowImplCocoa> parentWindow) {
|
WindowImplCocoa(Window::Kind kind, std::shared_ptr<WindowImplCocoa> parentWindow) {
|
||||||
ssView = [[SSView alloc] init];
|
ssView = [[SSView alloc] initWithKind:kind];
|
||||||
ssView.translatesAutoresizingMaskIntoConstraints = NO;
|
ssView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
ssView.receiver = this;
|
ssView.receiver = this;
|
||||||
|
|
||||||
|
@ -963,21 +1085,22 @@ public:
|
||||||
|
|
||||||
void ConfigureScrollbar(double min, double max, double pageSize) override {
|
void ConfigureScrollbar(double min, double max, double pageSize) override {
|
||||||
ssView.scrollerMin = min;
|
ssView.scrollerMin = min;
|
||||||
ssView.scrollerMax = max - pageSize;
|
ssView.scrollerSize = max + 1 - min;
|
||||||
[nsScroller setKnobProportion:(pageSize / (ssView.scrollerMax - ssView.scrollerMin))];
|
ssView.pageSize = pageSize;
|
||||||
|
nsScroller.knobProportion = pageSize / ssView.scrollerSize;
|
||||||
|
nsScroller.hidden = pageSize >= ssView.scrollerSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
double GetScrollbarPosition() override {
|
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 +
|
return ssView.scrollerMin +
|
||||||
[nsScroller doubleValue] * (ssView.scrollerMax - ssView.scrollerMin);
|
nsScroller.doubleValue * (ssView.scrollerSize - ssView.pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetScrollbarPosition(double pos) override {
|
void SetScrollbarPosition(double pos) override {
|
||||||
if(pos > ssView.scrollerMax)
|
nsScroller.doubleValue = (pos - ssView.scrollerMin) / ( ssView.scrollerSize - ssView.pageSize);
|
||||||
pos = ssView.scrollerMax;
|
|
||||||
if(GetScrollbarPosition() == pos)
|
|
||||||
return;
|
|
||||||
[nsScroller setDoubleValue:(pos / (ssView.scrollerMax - ssView.scrollerMin))];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Invalidate() override {
|
void Invalidate() override {
|
||||||
|
|
Loading…
Reference in New Issue