/* * Copyright 2019 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include #include "include/core/SkTypes.h" #include "tools/sk_app/mac/WindowContextFactory_mac.h" #include "tools/sk_app/mac/Window_mac.h" #include "tools/skui/ModifierKey.h" @interface WindowDelegate : NSObject - (WindowDelegate*)initWithWindow:(sk_app::Window_mac*)initWindow; @end @interface MainView : NSView - (MainView*)initWithWindow:(sk_app::Window_mac*)initWindow; @end /////////////////////////////////////////////////////////////////////////////// using sk_app::Window; namespace sk_app { SkTDynamicHash Window_mac::gWindowMap; Window* Window::CreateNativeWindow(void*) { Window_mac* window = new Window_mac(); if (!window->initWindow()) { delete window; return nullptr; } return window; } bool Window_mac::initWindow() { // we already have a window if (fWindow) { return true; } // Create a delegate to track certain events WindowDelegate* delegate = [[WindowDelegate alloc] initWithWindow:this]; if (nil == delegate) { return false; } // Create Cocoa window constexpr int initialWidth = 1280; constexpr int initialHeight = 960; NSRect windowRect = NSMakeRect(100, 100, initialWidth, initialHeight); NSUInteger windowStyle = (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask); fWindow = [[NSWindow alloc] initWithContentRect:windowRect styleMask:windowStyle backing:NSBackingStoreBuffered defer:NO]; if (nil == fWindow) { [delegate release]; return false; } // create view MainView* view = [[MainView alloc] initWithWindow:this]; if (nil == view) { [fWindow release]; [delegate release]; return false; } [fWindow setContentView:view]; [fWindow makeFirstResponder:view]; [fWindow setDelegate:delegate]; [fWindow setAcceptsMouseMovedEvents:YES]; [fWindow setRestorable:NO]; // Should be retained by window now [view release]; fWindowNumber = fWindow.windowNumber; gWindowMap.add(this); return true; } void Window_mac::closeWindow() { if (nil != fWindow) { gWindowMap.remove(fWindowNumber); if (sk_app::Window_mac::gWindowMap.count() < 1) { [NSApp terminate:fWindow]; } [fWindow close]; fWindow = nil; } } void Window_mac::setTitle(const char* title) { if (NSString* titleStr = [NSString stringWithUTF8String:title]) { [fWindow setTitle:titleStr]; } } void Window_mac::show() { [fWindow orderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; [fWindow makeKeyAndOrderFront:NSApp]; } bool Window_mac::attach(BackendType attachType) { this->initWindow(); window_context_factory::MacWindowInfo info; info.fMainView = [fWindow contentView]; switch (attachType) { #ifdef SK_DAWN case kDawn_BackendType: fWindowContext = MakeDawnMTLForMac(info, fRequestedDisplayParams); break; #endif #ifdef SK_VULKAN case kVulkan_BackendType: fWindowContext = MakeVulkanForMac(info, fRequestedDisplayParams); break; #endif #ifdef SK_METAL case kMetal_BackendType: fWindowContext = MakeMetalForMac(info, fRequestedDisplayParams); break; #ifdef SK_GRAPHITE_ENABLED case kGraphiteMetal_BackendType: fWindowContext = MakeGraphiteMetalForMac(info, fRequestedDisplayParams); break; #endif #endif #ifdef SK_GL case kNativeGL_BackendType: fWindowContext = MakeGLForMac(info, fRequestedDisplayParams); break; case kRaster_BackendType: fWindowContext = MakeRasterForMac(info, fRequestedDisplayParams); break; #endif default: SkASSERT_RELEASE(false); } this->onBackendCreated(); return SkToBool(fWindowContext); } float Window_mac::scaleFactor() const { return sk_app::GetBackingScaleFactor(fWindow.contentView); } void Window_mac::PaintWindows() { gWindowMap.foreach([&](Window_mac* window) { if (window->fIsContentInvalidated) { window->onPaint(); } }); } } // namespace sk_app /////////////////////////////////////////////////////////////////////////////// @implementation WindowDelegate { sk_app::Window_mac* fWindow; } - (WindowDelegate*)initWithWindow:(sk_app::Window_mac *)initWindow { fWindow = initWindow; return self; } - (void)windowDidResize:(NSNotification *)notification { NSView* view = fWindow->window().contentView; CGFloat scale = sk_app::GetBackingScaleFactor(view); fWindow->onResize(view.bounds.size.width * scale, view.bounds.size.height * scale); fWindow->inval(); } - (BOOL)windowShouldClose:(NSWindow*)sender { fWindow->closeWindow(); return FALSE; } @end /////////////////////////////////////////////////////////////////////////////// static skui::Key get_key(unsigned short vk) { // This will work with an ANSI QWERTY keyboard. // Something more robust would be needed to support alternate keyboards. static const struct { unsigned short fVK; skui::Key fKey; } gPair[] = { { kVK_Delete, skui::Key::kBack }, { kVK_Return, skui::Key::kOK }, { kVK_UpArrow, skui::Key::kUp }, { kVK_DownArrow, skui::Key::kDown }, { kVK_LeftArrow, skui::Key::kLeft }, { kVK_RightArrow, skui::Key::kRight }, { kVK_Tab, skui::Key::kTab }, { kVK_PageUp, skui::Key::kPageUp }, { kVK_PageDown, skui::Key::kPageDown }, { kVK_Home, skui::Key::kHome }, { kVK_End, skui::Key::kEnd }, { kVK_ForwardDelete, skui::Key::kDelete }, { kVK_Escape, skui::Key::kEscape }, { kVK_Shift, skui::Key::kShift }, { kVK_RightShift, skui::Key::kShift }, { kVK_Control, skui::Key::kCtrl }, { kVK_RightControl, skui::Key::kCtrl }, { kVK_Option, skui::Key::kOption }, { kVK_RightOption, skui::Key::kOption }, { kVK_Command, skui::Key::kSuper }, { kVK_RightCommand, skui::Key::kSuper }, { kVK_ANSI_A, skui::Key::kA }, { kVK_ANSI_C, skui::Key::kC }, { kVK_ANSI_V, skui::Key::kV }, { kVK_ANSI_X, skui::Key::kX }, { kVK_ANSI_Y, skui::Key::kY }, { kVK_ANSI_Z, skui::Key::kZ }, }; for (size_t i = 0; i < SK_ARRAY_COUNT(gPair); i++) { if (gPair[i].fVK == vk) { return gPair[i].fKey; } } return skui::Key::kNONE; } static skui::ModifierKey get_modifiers(const NSEvent* event) { NSUInteger modifierFlags = [event modifierFlags]; skui::ModifierKey modifiers = skui::ModifierKey::kNone; if (modifierFlags & NSEventModifierFlagCommand) { modifiers |= skui::ModifierKey::kCommand; } if (modifierFlags & NSEventModifierFlagShift) { modifiers |= skui::ModifierKey::kShift; } if (modifierFlags & NSEventModifierFlagControl) { modifiers |= skui::ModifierKey::kControl; } if (modifierFlags & NSEventModifierFlagOption) { modifiers |= skui::ModifierKey::kOption; } if ((NSKeyDown == [event type] || NSKeyUp == [event type]) && ![event isARepeat]) { modifiers |= skui::ModifierKey::kFirstPress; } return modifiers; } @implementation MainView { sk_app::Window_mac* fWindow; // A TrackingArea prevents us from capturing events outside the view NSTrackingArea* fTrackingArea; // We keep track of the state of the modifier keys on each event in order to synthesize // key-up/down events for each modifier. skui::ModifierKey fLastModifiers; } - (MainView*)initWithWindow:(sk_app::Window_mac *)initWindow { self = [super init]; fWindow = initWindow; fTrackingArea = nil; [self updateTrackingAreas]; return self; } - (void)dealloc { [fTrackingArea release]; [super dealloc]; } - (BOOL)isOpaque { return YES; } - (BOOL)canBecomeKeyView { return YES; } - (BOOL)acceptsFirstResponder { return YES; } - (void)updateTrackingAreas { if (fTrackingArea != nil) { [self removeTrackingArea:fTrackingArea]; [fTrackingArea release]; } const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingEnabledDuringMouseDrag | NSTrackingCursorUpdate | NSTrackingInVisibleRect | NSTrackingAssumeInside; fTrackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; [self addTrackingArea:fTrackingArea]; [super updateTrackingAreas]; } - (skui::ModifierKey) updateModifierKeys:(NSEvent*) event { using sknonstd::Any; skui::ModifierKey modifiers = get_modifiers(event); skui::ModifierKey changed = modifiers ^ fLastModifiers; fLastModifiers = modifiers; struct ModMap { skui::ModifierKey modifier; skui::Key key; }; // Map each modifier bit to the equivalent skui Key and send key-up/down events. for (const ModMap& cur : {ModMap{skui::ModifierKey::kCommand, skui::Key::kSuper}, ModMap{skui::ModifierKey::kShift, skui::Key::kShift}, ModMap{skui::ModifierKey::kControl, skui::Key::kCtrl}, ModMap{skui::ModifierKey::kOption, skui::Key::kOption}}) { if (Any(changed & cur.modifier)) { const skui::InputState state = Any(modifiers & cur.modifier) ? skui::InputState::kDown : skui::InputState::kUp; (void) fWindow->onKey(cur.key, state, modifiers); } } return modifiers; } - (BOOL)performKeyEquivalent:(NSEvent *)event { [self updateModifierKeys:event]; // By default, unhandled key equivalents send -keyDown events; unfortunately, they do not send // a matching -keyUp. In other words, we can claim that we didn't handle the event and OS X will // turn this event into a -keyDown automatically, but we need to synthesize a matching -keyUp on // a later frame. Since we only read the modifiers and key code from the event, we can reuse // this "key-equivalent" event as a "key up". [self performSelector:@selector(keyUp:) withObject:event afterDelay:0.1]; return NO; } - (void)keyDown:(NSEvent *)event { skui::ModifierKey modifiers = [self updateModifierKeys:event]; skui::Key key = get_key([event keyCode]); if (key != skui::Key::kNONE) { if (!fWindow->onKey(key, skui::InputState::kDown, modifiers)) { if (skui::Key::kEscape == key) { [NSApp terminate:fWindow->window()]; } } } NSString* characters = [event charactersIgnoringModifiers]; NSUInteger len = [characters length]; if (len > 0) { unichar* charBuffer = new unichar[len+1]; [characters getCharacters:charBuffer range:NSMakeRange(0, len)]; for (NSUInteger i = 0; i < len; ++i) { (void) fWindow->onChar((SkUnichar) charBuffer[i], modifiers); } delete [] charBuffer; } } - (void)keyUp:(NSEvent *)event { skui::ModifierKey modifiers = [self updateModifierKeys:event]; skui::Key key = get_key([event keyCode]); if (key != skui::Key::kNONE) { (void) fWindow->onKey(key, skui::InputState::kUp, modifiers); } } -(void)flagsChanged:(NSEvent *)event { [self updateModifierKeys:event]; } - (void)mouseDown:(NSEvent *)event { NSView* view = fWindow->window().contentView; CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); skui::ModifierKey modifiers = [self updateModifierKeys:event]; const NSPoint pos = [event locationInWindow]; const NSRect rect = [view frame]; fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, skui::InputState::kDown, modifiers); } - (void)mouseUp:(NSEvent *)event { NSView* view = fWindow->window().contentView; CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); skui::ModifierKey modifiers = [self updateModifierKeys:event]; const NSPoint pos = [event locationInWindow]; const NSRect rect = [view frame]; fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, skui::InputState::kUp, modifiers); } - (void)mouseDragged:(NSEvent *)event { [self updateModifierKeys:event]; [self mouseMoved:event]; } - (void)mouseMoved:(NSEvent *)event { NSView* view = fWindow->window().contentView; CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); skui::ModifierKey modifiers = [self updateModifierKeys:event]; const NSPoint pos = [event locationInWindow]; const NSRect rect = [view frame]; fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, skui::InputState::kMove, modifiers); } - (void)scrollWheel:(NSEvent *)event { skui::ModifierKey modifiers = [self updateModifierKeys:event]; // TODO: support hasPreciseScrollingDeltas? fWindow->onMouseWheel([event scrollingDeltaY], modifiers); } - (void)drawRect:(NSRect)rect { fWindow->onPaint(); } @end