mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
d7a1ec7461
Widgets such as Editor use certain key events such as the backspace key to implement text editing. On macOS, such key events are sometimes used by an input method, and in those cases the key effect would be applied twice: first by the IME and then the Editor. Report such key events through the doCommandBySelector callback, which receives key events not handled by the IME. References: https://todo.sr.ht/~eliasnaur/gio/616 Signed-off-by: Elias Naur <mail@eliasnaur.com>
503 lines
16 KiB
Objective-C
503 lines
16 KiB
Objective-C
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
// +build darwin,!ios
|
|
|
|
#import <AppKit/AppKit.h>
|
|
|
|
#include "_cgo_export.h"
|
|
|
|
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
|
|
|
|
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
|
|
@end
|
|
|
|
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
|
|
@end
|
|
|
|
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
|
@property NSEvent *lastKeyDown;
|
|
@property uintptr_t handle;
|
|
@property BOOL presentWithTrans;
|
|
@end
|
|
|
|
@implementation GioWindowDelegate
|
|
- (void)windowWillMiniaturize:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
gio_onDraw(view.handle);
|
|
}
|
|
- (void)windowDidDeminiaturize:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
gio_onDraw(view.handle);
|
|
}
|
|
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
gio_onDraw(view.handle);
|
|
}
|
|
- (void)windowWillExitFullScreen:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
gio_onDraw(view.handle);
|
|
}
|
|
- (void)windowDidChangeScreen:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
|
|
GioView *view = (GioView *)window.contentView;
|
|
gio_onChangeScreen(view.handle, dispID);
|
|
}
|
|
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
if ([window firstResponder] == view) {
|
|
gio_onFocus(view.handle, 1);
|
|
}
|
|
}
|
|
- (void)windowDidResignKey:(NSNotification *)notification {
|
|
NSWindow *window = (NSWindow *)[notification object];
|
|
GioView *view = (GioView *)window.contentView;
|
|
if ([window firstResponder] == view) {
|
|
gio_onFocus(view.handle, 0);
|
|
}
|
|
}
|
|
@end
|
|
|
|
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
|
|
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
|
|
if (!event.hasPreciseScrollingDeltas) {
|
|
// dx and dy are in rows and columns.
|
|
dx *= 10;
|
|
dy *= 10;
|
|
}
|
|
// Origin is in the lower left corner. Convert to upper left.
|
|
CGFloat height = view.bounds.size.height;
|
|
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
|
}
|
|
|
|
@implementation GioView
|
|
- (void)setFrameSize:(NSSize)newSize {
|
|
[super setFrameSize:newSize];
|
|
[self setNeedsDisplay:YES];
|
|
}
|
|
// drawRect is called when OpenGL is used, displayLayer otherwise.
|
|
// Don't know why.
|
|
- (void)drawRect:(NSRect)r {
|
|
gio_onDraw(self.handle);
|
|
}
|
|
- (void)displayLayer:(CALayer *)layer {
|
|
layer.contentsScale = self.window.backingScaleFactor;
|
|
gio_onDraw(self.handle);
|
|
}
|
|
- (CALayer *)makeBackingLayer {
|
|
CALayer *layer = gio_layerFactory(self.presentWithTrans);
|
|
layer.delegate = self;
|
|
return layer;
|
|
}
|
|
- (void)viewDidMoveToWindow {
|
|
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
|
|
}
|
|
- (void)mouseDown:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
|
}
|
|
- (void)mouseUp:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
|
}
|
|
- (void)rightMouseDown:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
|
}
|
|
- (void)rightMouseUp:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
|
}
|
|
- (void)otherMouseDown:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
|
}
|
|
- (void)otherMouseUp:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
|
}
|
|
- (void)mouseMoved:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
|
}
|
|
- (void)mouseDragged:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
|
}
|
|
- (void)rightMouseDragged:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
|
}
|
|
- (void)otherMouseDragged:(NSEvent *)event {
|
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
|
}
|
|
- (void)scrollWheel:(NSEvent *)event {
|
|
CGFloat dx = -event.scrollingDeltaX;
|
|
CGFloat dy = -event.scrollingDeltaY;
|
|
handleMouse(self, event, MOUSE_SCROLL, dx, dy);
|
|
}
|
|
- (void)keyDown:(NSEvent *)event {
|
|
// Stash the event for use by doCommandBySelector.
|
|
self.lastKeyDown = event;
|
|
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
|
|
self.lastKeyDown = nil;
|
|
NSString *keys = [event charactersIgnoringModifiers];
|
|
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
|
|
}
|
|
- (void)keyUp:(NSEvent *)event {
|
|
NSString *keys = [event charactersIgnoringModifiers];
|
|
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
|
|
}
|
|
- (void)insertText:(id)string {
|
|
gio_onText(self.handle, (__bridge CFTypeRef)string);
|
|
}
|
|
- (void)doCommandBySelector:(SEL)action {
|
|
NSEvent *event = self.lastKeyDown;
|
|
if (event == nil) {
|
|
return;
|
|
}
|
|
int key = 0;
|
|
if (action == @selector(deleteBackward:)) {
|
|
key = KEY_DELETE_BACKWARD;
|
|
} else if (action == @selector(deleteForward:)) {
|
|
key = NSDeleteFunctionKey;
|
|
} else if (action == @selector(insertNewline:)) {
|
|
NSString *keys = [event charactersIgnoringModifiers];
|
|
if ([keys isEqualToString:@"\r"]) {
|
|
key = KEY_RETURN;
|
|
} else {
|
|
key = KEY_ENTER;
|
|
}
|
|
} else if (action == @selector(moveUp:)) {
|
|
key = NSUpArrowFunctionKey;
|
|
} else if (action == @selector(moveDown:)) {
|
|
key = NSDownArrowFunctionKey;
|
|
} else if (action == @selector(moveLeft:)) {
|
|
key = NSLeftArrowFunctionKey;
|
|
} else if (action == @selector(moveRight:)) {
|
|
key = NSRightArrowFunctionKey;
|
|
} else if (action == @selector(cancelOperation:)) {
|
|
key = KEY_ESCAPE;
|
|
} else if (action == @selector(insertTab:)) {
|
|
key = KEY_TAB;
|
|
} else if (action == @selector(scrollToBeginningOfDocument:)) {
|
|
key = NSHomeFunctionKey;
|
|
} else if (action == @selector(scrollToEndOfDocument:)) {
|
|
key = NSEndFunctionKey;
|
|
} else if (action == @selector(scrollPageUp:)) {
|
|
key = NSPageUpFunctionKey;
|
|
} else if (action == @selector(scrollPageDown:)) {
|
|
key = NSPageDownFunctionKey;
|
|
} else {
|
|
return;
|
|
}
|
|
gio_onCommandBySelector(self.handle, key, [event timestamp], [event modifierFlags]);
|
|
}
|
|
|
|
- (BOOL)hasMarkedText {
|
|
int res = gio_hasMarkedText(self.handle);
|
|
return res ? YES : NO;
|
|
}
|
|
- (NSRange)markedRange {
|
|
return gio_markedRange(self.handle);
|
|
}
|
|
- (NSRange)selectedRange {
|
|
return gio_selectedRange(self.handle);
|
|
}
|
|
- (void)unmarkText {
|
|
gio_unmarkText(self.handle);
|
|
}
|
|
- (void)setMarkedText:(id)string
|
|
selectedRange:(NSRange)selRange
|
|
replacementRange:(NSRange)replaceRange {
|
|
NSString *str;
|
|
// string is either an NSAttributedString or an NSString.
|
|
if ([string isKindOfClass:[NSAttributedString class]]) {
|
|
str = [string string];
|
|
} else {
|
|
str = string;
|
|
}
|
|
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
|
|
}
|
|
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
|
return nil;
|
|
}
|
|
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
|
actualRange:(NSRangePointer)actualRange {
|
|
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
|
|
return [[NSAttributedString alloc] initWithString:str attributes:nil];
|
|
}
|
|
- (void)insertText:(id)string
|
|
replacementRange:(NSRange)replaceRange {
|
|
NSEvent *event = self.lastKeyDown;
|
|
if (event == nil) {
|
|
return;
|
|
}
|
|
NSString *str;
|
|
// string is either an NSAttributedString or an NSString.
|
|
if ([string isKindOfClass:[NSAttributedString class]]) {
|
|
str = [string string];
|
|
} else {
|
|
str = string;
|
|
}
|
|
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange, [event timestamp], [event modifierFlags]);
|
|
}
|
|
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
|
return gio_characterIndexForPoint(self.handle, p);
|
|
}
|
|
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
|
actualRange:(NSRangePointer)actual {
|
|
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
|
|
r = [self convertRect:r toView:nil];
|
|
return [[self window] convertRectToScreen:r];
|
|
}
|
|
- (void)applicationWillUnhide:(NSNotification *)notification {
|
|
gio_onDraw(self.handle);
|
|
}
|
|
- (void)applicationDidHide:(NSNotification *)notification {
|
|
gio_onDraw(self.handle);
|
|
}
|
|
- (void)dealloc {
|
|
gio_onDestroy(self.handle);
|
|
}
|
|
- (BOOL) becomeFirstResponder {
|
|
gio_onFocus(self.handle, 1);
|
|
return [super becomeFirstResponder];
|
|
}
|
|
- (BOOL) resignFirstResponder {
|
|
gio_onFocus(self.handle, 0);
|
|
return [super resignFirstResponder];
|
|
}
|
|
@end
|
|
|
|
// Delegates are weakly referenced from their peers. Nothing
|
|
// else holds a strong reference to our window delegate, so
|
|
// keep a single global reference instead.
|
|
static GioWindowDelegate *globalWindowDel;
|
|
|
|
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
|
|
gio_onFrameCallback(dl);
|
|
return kCVReturnSuccess;
|
|
}
|
|
|
|
CFTypeRef gio_createDisplayLink(void) {
|
|
CVDisplayLinkRef dl;
|
|
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
|
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
|
|
return dl;
|
|
}
|
|
|
|
int gio_startDisplayLink(CFTypeRef dl) {
|
|
return CVDisplayLinkStart((CVDisplayLinkRef)dl);
|
|
}
|
|
|
|
int gio_stopDisplayLink(CFTypeRef dl) {
|
|
return CVDisplayLinkStop((CVDisplayLinkRef)dl);
|
|
}
|
|
|
|
void gio_releaseDisplayLink(CFTypeRef dl) {
|
|
CVDisplayLinkRelease((CVDisplayLinkRef)dl);
|
|
}
|
|
|
|
void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
|
|
CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did);
|
|
}
|
|
|
|
void gio_hideCursor() {
|
|
@autoreleasepool {
|
|
[NSCursor hide];
|
|
}
|
|
}
|
|
|
|
void gio_showCursor() {
|
|
@autoreleasepool {
|
|
[NSCursor unhide];
|
|
}
|
|
}
|
|
|
|
// some cursors are not public, this tries to use a private cursor
|
|
// and uses fallback when the use of private cursor fails.
|
|
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
|
|
if ([NSCursor respondsToSelector:cursorName]) {
|
|
id object = [NSCursor performSelector:cursorName];
|
|
if ([object isKindOfClass:[NSCursor class]]) {
|
|
[(NSCursor*)object set];
|
|
return;
|
|
}
|
|
}
|
|
[fallback set];
|
|
}
|
|
|
|
void gio_setCursor(NSUInteger curID) {
|
|
@autoreleasepool {
|
|
switch (curID) {
|
|
case 0: // pointer.CursorDefault
|
|
[NSCursor.arrowCursor set];
|
|
break;
|
|
// case 1: // pointer.CursorNone
|
|
case 2: // pointer.CursorText
|
|
[NSCursor.IBeamCursor set];
|
|
break;
|
|
case 3: // pointer.CursorVerticalText
|
|
[NSCursor.IBeamCursorForVerticalLayout set];
|
|
break;
|
|
case 4: // pointer.CursorPointer
|
|
[NSCursor.pointingHandCursor set];
|
|
break;
|
|
case 5: // pointer.CursorCrosshair
|
|
[NSCursor.crosshairCursor set];
|
|
break;
|
|
case 6: // pointer.CursorAllScroll
|
|
// For some reason, using _moveCursor fails on Monterey.
|
|
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
|
|
[NSCursor.arrowCursor set];
|
|
break;
|
|
case 7: // pointer.CursorColResize
|
|
[NSCursor.resizeLeftRightCursor set];
|
|
break;
|
|
case 8: // pointer.CursorRowResize
|
|
[NSCursor.resizeUpDownCursor set];
|
|
break;
|
|
case 9: // pointer.CursorGrab
|
|
[NSCursor.openHandCursor set];
|
|
break;
|
|
case 10: // pointer.CursorGrabbing
|
|
[NSCursor.closedHandCursor set];
|
|
break;
|
|
case 11: // pointer.CursorNotAllowed
|
|
[NSCursor.operationNotAllowedCursor set];
|
|
break;
|
|
case 12: // pointer.CursorWait
|
|
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
|
break;
|
|
case 13: // pointer.CursorProgress
|
|
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
|
break;
|
|
case 14: // pointer.CursorNorthWestResize
|
|
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
case 15: // pointer.CursorNorthEastResize
|
|
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
case 16: // pointer.CursorSouthWestResize
|
|
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
case 17: // pointer.CursorSouthEastResize
|
|
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
case 18: // pointer.CursorNorthSouthResize
|
|
[NSCursor.resizeUpDownCursor set];
|
|
break;
|
|
case 19: // pointer.CursorEastWestResize
|
|
[NSCursor.resizeLeftRightCursor set];
|
|
break;
|
|
case 20: // pointer.CursorWestResize
|
|
[NSCursor.resizeLeftCursor set];
|
|
break;
|
|
case 21: // pointer.CursorEastResize
|
|
[NSCursor.resizeRightCursor set];
|
|
break;
|
|
case 22: // pointer.CursorNorthResize
|
|
[NSCursor.resizeUpCursor set];
|
|
break;
|
|
case 23: // pointer.CursorSouthResize
|
|
[NSCursor.resizeDownCursor set];
|
|
break;
|
|
case 24: // pointer.CursorNorthEastSouthWestResize
|
|
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
case 25: // pointer.CursorNorthWestSouthEastResize
|
|
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
|
|
break;
|
|
default:
|
|
[NSCursor.arrowCursor set];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) {
|
|
@autoreleasepool {
|
|
NSRect rect = NSMakeRect(0, 0, width, height);
|
|
NSUInteger styleMask = NSTitledWindowMask |
|
|
NSResizableWindowMask |
|
|
NSMiniaturizableWindowMask |
|
|
NSClosableWindowMask;
|
|
|
|
NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
|
|
styleMask:styleMask
|
|
backing:NSBackingStoreBuffered
|
|
defer:NO];
|
|
if (minWidth > 0 || minHeight > 0) {
|
|
window.contentMinSize = NSMakeSize(minWidth, minHeight);
|
|
}
|
|
if (maxWidth > 0 || maxHeight > 0) {
|
|
window.contentMaxSize = NSMakeSize(maxWidth, maxHeight);
|
|
}
|
|
[window setAcceptsMouseMovedEvents:YES];
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
[window setContentView:view];
|
|
window.delegate = globalWindowDel;
|
|
return (__bridge_retained CFTypeRef)window;
|
|
}
|
|
}
|
|
|
|
CFTypeRef gio_createView(int presentWithTrans) {
|
|
@autoreleasepool {
|
|
NSRect frame = NSMakeRect(0, 0, 0, 0);
|
|
GioView* view = [[GioView alloc] initWithFrame:frame];
|
|
view.presentWithTrans = presentWithTrans ? YES : NO;
|
|
view.wantsLayer = YES;
|
|
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:view
|
|
selector:@selector(applicationWillUnhide:)
|
|
name:NSApplicationWillUnhideNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:view
|
|
selector:@selector(applicationDidHide:)
|
|
name:NSApplicationDidHideNotification
|
|
object:nil];
|
|
return CFBridgingRetain(view);
|
|
}
|
|
}
|
|
|
|
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
|
@autoreleasepool {
|
|
GioView *v = (__bridge GioView *)viewRef;
|
|
v.handle = handle;
|
|
}
|
|
}
|
|
|
|
@implementation GioAppDelegate
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
gio_onFinishLaunching();
|
|
}
|
|
@end
|
|
|
|
void gio_main() {
|
|
@autoreleasepool {
|
|
[NSApplication sharedApplication];
|
|
GioAppDelegate *del = [[GioAppDelegate alloc] init];
|
|
[NSApp setDelegate:del];
|
|
|
|
NSMenuItem *mainMenu = [NSMenuItem new];
|
|
|
|
NSMenu *menu = [NSMenu new];
|
|
NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide"
|
|
action:@selector(hide:)
|
|
keyEquivalent:@"h"];
|
|
[menu addItem:hideMenuItem];
|
|
NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
|
|
action:@selector(terminate:)
|
|
keyEquivalent:@"q"];
|
|
[menu addItem:quitMenuItem];
|
|
[mainMenu setSubmenu:menu];
|
|
NSMenu *menuBar = [NSMenu new];
|
|
[menuBar addItem:mainMenu];
|
|
[NSApp setMainMenu:menuBar];
|
|
|
|
globalWindowDel = [[GioWindowDelegate alloc] init];
|
|
|
|
[NSApp run];
|
|
}
|
|
}
|