// 9 september 2015 #import "uipriv_darwin.h" // 10.8 fixups #define NSEventModifierFlags NSUInteger @interface areaView : NSView { uiArea *libui_a; NSTrackingArea *libui_ta; NSSize libui_ss; BOOL libui_enabled; } - (id)initWithFrame:(NSRect)r area:(uiArea *)a; - (uiModifiers)parseModifiers:(NSEvent *)e; - (void)doMouseEvent:(NSEvent *)e; - (int)sendKeyEvent:(uiAreaKeyEvent *)ke; - (int)doKeyDownUp:(NSEvent *)e up:(int)up; - (int)doKeyDown:(NSEvent *)e; - (int)doKeyUp:(NSEvent *)e; - (int)doFlagsChanged:(NSEvent *)e; - (void)setupNewTrackingArea; - (void)setScrollingSize:(NSSize)s; - (BOOL)isEnabled; - (void)setEnabled:(BOOL)e; @end struct uiArea { uiDarwinControl c; NSView *view; // either sv or area depending on whether it is scrolling NSScrollView *sv; areaView *area; uiprivScrollViewData *d; uiAreaHandler *ah; BOOL scrolling; NSEvent *dragevent; }; @implementation areaView - (id)initWithFrame:(NSRect)r area:(uiArea *)a { self = [super initWithFrame:r]; if (self) { self->libui_a = a; [self setupNewTrackingArea]; self->libui_ss = r.size; self->libui_enabled = YES; } return self; } - (void)drawRect:(NSRect)r { uiArea *a = self->libui_a; CGContextRef c; uiAreaDrawParams dp; c = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort]; // see draw.m under text for why we need the height dp.Context = uiprivDrawNewContext(c, [self bounds].size.height); dp.AreaWidth = 0; dp.AreaHeight = 0; if (!a->scrolling) { dp.AreaWidth = [self frame].size.width; dp.AreaHeight = [self frame].size.height; } dp.ClipX = r.origin.x; dp.ClipY = r.origin.y; dp.ClipWidth = r.size.width; dp.ClipHeight = r.size.height; // no need to save or restore the graphics state to reset transformations; Cocoa creates a brand-new context each time (*(a->ah->Draw))(a->ah, a, &dp); uiprivDrawFreeContext(dp.Context); } - (BOOL)isFlipped { return YES; } - (BOOL)acceptsFirstResponder { return YES; } - (uiModifiers)parseModifiers:(NSEvent *)e { NSEventModifierFlags mods; uiModifiers m; m = 0; mods = [e modifierFlags]; if ((mods & NSControlKeyMask) != 0) m |= uiModifierCtrl; if ((mods & NSAlternateKeyMask) != 0) m |= uiModifierAlt; if ((mods & NSShiftKeyMask) != 0) m |= uiModifierShift; if ((mods & NSCommandKeyMask) != 0) m |= uiModifierSuper; return m; } - (void)setupNewTrackingArea { self->libui_ta = [[NSTrackingArea alloc] initWithRect:[self bounds] options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag) owner:self userInfo:nil]; [self addTrackingArea:self->libui_ta]; } - (void)updateTrackingAreas { [self removeTrackingArea:self->libui_ta]; [self->libui_ta release]; [self setupNewTrackingArea]; } // capture on drag is done automatically on OS X - (void)doMouseEvent:(NSEvent *)e { uiArea *a = self->libui_a; uiAreaMouseEvent me; NSPoint point; unsigned int buttonNumber; NSUInteger pmb; unsigned int i, max; // this will convert point to drawing space // thanks swillits in irc.freenode.net/#macdev point = [self convertPoint:[e locationInWindow] fromView:nil]; me.X = point.x; me.Y = point.y; me.AreaWidth = 0; me.AreaHeight = 0; if (!a->scrolling) { me.AreaWidth = [self frame].size.width; me.AreaHeight = [self frame].size.height; } buttonNumber = [e buttonNumber] + 1; // swap button numbers 2 and 3 (right and middle) if (buttonNumber == 2) buttonNumber = 3; else if (buttonNumber == 3) buttonNumber = 2; me.Down = 0; me.Up = 0; me.Count = 0; if (@available(macOS 10.12, *)) { switch ([e type]) { case NSEventTypeLeftMouseDown: case NSEventTypeRightMouseDown: case NSEventTypeOtherMouseDown: me.Down = buttonNumber; me.Count = [e clickCount]; break; case NSEventTypeLeftMouseUp: case NSEventTypeRightMouseUp: case NSEventTypeOtherMouseUp: me.Up = buttonNumber; break; case NSEventTypeLeftMouseDragged: case NSEventTypeRightMouseDragged: case NSEventTypeOtherMouseDragged: // we include the button that triggered the dragged event in the Held fields buttonNumber = 0; break; } } else { NSEventType type = [e type]; if (type == NSLeftMouseDown || type == NSRightMouseDown || type == NSOtherMouseDown) { me.Down = buttonNumber; me.Count = [e clickCount]; } else if (type == NSLeftMouseUp || type == NSRightMouseUp || type == NSOtherMouseUp) { me.Up = buttonNumber; } else if (type == NSLeftMouseDragged || type == NSRightMouseDragged || type == NSOtherMouseDragged) { // we include the button that triggered the dragged event in the Held fields buttonNumber = 0; } } me.Modifiers = [self parseModifiers:e]; pmb = [NSEvent pressedMouseButtons]; me.Held1To64 = 0; if (buttonNumber != 1 && (pmb & 1) != 0) me.Held1To64 |= 1; if (buttonNumber != 2 && (pmb & 4) != 0) me.Held1To64 |= 2; if (buttonNumber != 3 && (pmb & 2) != 0) me.Held1To64 |= 4; // buttons 4..32 // https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/index.html#//apple_ref/c/tdef/CGMouseButton says Quartz only supports up to 32 buttons max = 32; for (i = 4; i <= max; i++) { uint64_t j; if (buttonNumber == i) continue; j = 1 << (i - 1); if ((pmb & j) != 0) me.Held1To64 |= j; } if (self->libui_enabled) { // and allow dragging here a->dragevent = e; (*(a->ah->MouseEvent))(a->ah, a, &me); a->dragevent = nil; } } #define mouseEvent(name) \ - (void)name:(NSEvent *)e \ { \ [self doMouseEvent:e]; \ } mouseEvent(mouseMoved) mouseEvent(mouseDragged) mouseEvent(rightMouseDragged) mouseEvent(otherMouseDragged) mouseEvent(mouseDown) mouseEvent(rightMouseDown) mouseEvent(otherMouseDown) mouseEvent(mouseUp) mouseEvent(rightMouseUp) mouseEvent(otherMouseUp) - (void)mouseEntered:(NSEvent *)e { uiArea *a = self->libui_a; if (self->libui_enabled) (*(a->ah->MouseCrossed))(a->ah, a, 0); } - (void)mouseExited:(NSEvent *)e { uiArea *a = self->libui_a; if (self->libui_enabled) (*(a->ah->MouseCrossed))(a->ah, a, 1); } // note: there is no equivalent to WM_CAPTURECHANGED on Mac OS X; there literally is no way to break a grab like that // even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons // therefore, no DragBroken() - (int)sendKeyEvent:(uiAreaKeyEvent *)ke { uiArea *a = self->libui_a; return (*(a->ah->KeyEvent))(a->ah, a, ke); } - (int)doKeyDownUp:(NSEvent *)e up:(int)up { uiAreaKeyEvent ke; ke.Key = 0; ke.ExtKey = 0; ke.Modifier = 0; ke.Modifiers = [self parseModifiers:e]; ke.Up = up; if (!uiprivFromKeycode([e keyCode], &ke)) return 0; return [self sendKeyEvent:&ke]; } - (int)doKeyDown:(NSEvent *)e { return [self doKeyDownUp:e up:0]; } - (int)doKeyUp:(NSEvent *)e { return [self doKeyDownUp:e up:1]; } - (int)doFlagsChanged:(NSEvent *)e { uiAreaKeyEvent ke; uiModifiers whichmod; ke.Key = 0; ke.ExtKey = 0; // Mac OS X sends this event on both key up and key down. // Fortunately -[e keyCode] IS valid here, so we can simply map from key code to Modifiers, get the value of [e modifierFlags], and check if the respective bit is set or not — that will give us the up/down state if (!uiprivKeycodeModifier([e keyCode], &whichmod)) return 0; ke.Modifier = whichmod; ke.Modifiers = [self parseModifiers:e]; ke.Up = (ke.Modifiers & ke.Modifier) == 0; // and then drop the current modifier from Modifiers ke.Modifiers &= ~ke.Modifier; return [self sendKeyEvent:&ke]; } - (void)setFrameSize:(NSSize)size { uiArea *a = self->libui_a; [super setFrameSize:size]; if (!a->scrolling) // we must redraw everything on resize because Windows requires it [self setNeedsDisplay:YES]; } - (void)setScrollingSize:(NSSize)s { self->libui_ss = s; [self setFrameSize:s]; } - (NSSize)intrinsicContentSize { if (!self->libui_a->scrolling) return [super intrinsicContentSize]; return self->libui_ss; } - (BOOL)becomeFirstResponder { return [self isEnabled]; } - (BOOL)isEnabled { return self->libui_enabled; } - (void)setEnabled:(BOOL)e { self->libui_enabled = e; if (!self->libui_enabled && [self window] != nil) if ([[self window] firstResponder] == self) [[self window] makeFirstResponder:nil]; } @end uiDarwinControlAllDefaultsExceptDestroy(uiArea, view) static void uiAreaDestroy(uiControl *c) { uiArea *a = uiArea(c); if (a->scrolling) uiprivScrollViewFreeData(a->sv, a->d); [a->area release]; if (a->scrolling) [a->sv release]; uiFreeControl(uiControl(a)); } // called by subclasses of -[NSApplication sendEvent:] // by default, NSApplication eats some key events // this prevents that from happening with uiArea // see http://stackoverflow.com/questions/24099063/how-do-i-detect-keyup-in-my-nsview-with-the-command-key-held and http://lists.apple.com/archives/cocoa-dev/2003/Oct/msg00442.html int uiprivSendAreaEvents(NSEvent *e) { NSEventType type; id focused; areaView *view; type = [e type]; if (type != NSKeyDown && type != NSKeyUp && type != NSFlagsChanged) return 0; focused = [[e window] firstResponder]; if (focused == nil) return 0; if (![focused isKindOfClass:[areaView class]]) return 0; view = (areaView *) focused; if (@available(macOS 10.12, *)) { switch (type) { case NSEventTypeKeyDown: return [view doKeyDown:e]; case NSEventTypeKeyUp: return [view doKeyUp:e]; case NSEventTypeFlagsChanged: return [view doFlagsChanged:e]; } } else { if (type == NSKeyDown) return [view doKeyDown:e]; if (type == NSKeyUp) return [view doKeyUp:e]; if (type == NSFlagsChanged) return [view doFlagsChanged:e]; } return 0; } void uiAreaSetSize(uiArea *a, int width, int height) { if (!a->scrolling) uiprivUserBug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a); [a->area setScrollingSize:NSMakeSize(width, height)]; } void uiAreaQueueRedrawAll(uiArea *a) { [a->area setNeedsDisplay:YES]; } void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height) { if (!a->scrolling) uiprivUserBug("You cannot call uiAreaScrollTo() on a non-scrolling uiArea. (area: %p)", a); [a->area scrollRectToVisible:NSMakeRect(x, y, width, height)]; // don't worry about the return value; it just says whether scrolling was needed } void uiAreaBeginUserWindowMove(uiArea *a) { uiprivNSWindow *w; w = (uiprivNSWindow *) [a->area window]; if (w == nil) return; // TODO if (a->dragevent == nil) return; // TODO [w uiprivDoMove:a->dragevent]; } void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge) { uiprivNSWindow *w; w = (uiprivNSWindow *) [a->area window]; if (w == nil) return; // TODO if (a->dragevent == nil) return; // TODO [w uiprivDoResize:a->dragevent on:edge]; } uiArea *uiNewArea(uiAreaHandler *ah) { uiArea *a; uiDarwinNewControl(uiArea, a); a->ah = ah; a->scrolling = NO; a->area = [[areaView alloc] initWithFrame:NSZeroRect area:a]; a->view = a->area; return a; } uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height) { uiArea *a; uiprivScrollViewCreateParams p; uiDarwinNewControl(uiArea, a); a->ah = ah; a->scrolling = YES; a->area = [[areaView alloc] initWithFrame:NSMakeRect(0, 0, width, height) area:a]; memset(&p, 0, sizeof (uiprivScrollViewCreateParams)); p.DocumentView = a->area; p.BackgroundColor = [NSColor controlColor]; p.DrawsBackground = 1; p.Bordered = NO; p.HScroll = YES; p.VScroll = YES; a->sv = uiprivMkScrollView(&p, &(a->d)); a->view = a->sv; return a; }