1 /** 2 * Cocoa window implementation. 3 * Copyright: Copyright Guillaume Piolat 2015 - 2021. 4 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 5 */ 6 module dplug.window.cocoawindow; 7 8 import core.stdc.stdlib; 9 10 import std.string; 11 import std.uuid; 12 13 import dplug.math.vector; 14 import dplug.math.box; 15 import dplug.graphics.image; 16 17 import dplug.core.sync; 18 import dplug.core.runtime; 19 import dplug.core.nogc; 20 import dplug.core.random; 21 import dplug.core.thread; 22 import dplug.window.window; 23 24 import derelict.cocoa; 25 26 version(legacyMouseCursor) 27 { 28 version = noCursors; // FUTURE: tell to replace with Dplug_NoMouseCursor 29 } 30 else version(Dplug_NoMouseCursor) 31 { 32 version = noCursors; 33 } 34 35 36 version = useCoreGraphicsContext; 37 38 final class CocoaWindow : IWindow 39 { 40 nothrow: 41 @nogc: 42 private: 43 IWindowListener _listener; 44 45 NSColorSpace _nsColorSpace; 46 CGColorSpaceRef _cgColorSpaceRef; 47 NSData _imageData; 48 NSString _logFormatStr; 49 50 // Only used by host windows 51 NSWindow _nsWindow; 52 NSApplication _nsApplication; 53 54 DPlugCustomView _view = null; 55 56 bool _terminated = false; 57 58 int _lastMouseX, _lastMouseY; 59 bool _firstMouseMove = true; 60 61 int _width; 62 int _height; 63 64 ImageRef!RGBA _wfb; 65 66 uint _timeAtCreationInMs; 67 uint _lastMeasturedTimeInMs; 68 bool _dirtyAreasAreNotYetComputed; 69 70 bool _isHostWindow; 71 bool _drawRectWorkaround; // See Issue #505 & #705, drawRect: returning always one big rectangle, killing CPU 72 73 MouseCursor _lastMouseCursor; 74 75 public: 76 77 this(WindowUsage usage, void* parentWindow, IWindowListener listener, int width, int height) 78 { 79 _isHostWindow = (usage == WindowUsage.host); 80 81 _listener = listener; 82 83 acquireCocoaFunctions(); 84 acquireCoreGraphicsFunctions(); 85 NSApplicationLoad(); // to use Cocoa in Carbon applications 86 bool parentViewExists = parentWindow !is null; 87 88 _width = 0; 89 _height = 0; 90 91 _nsColorSpace = NSColorSpace.sRGBColorSpace(); 92 // hopefully not null else the colors will be brighter 93 _cgColorSpaceRef = _nsColorSpace.CGColorSpace(); 94 95 _logFormatStr = NSString.stringWith("%@"); 96 97 _timeAtCreationInMs = getTimeMs(); 98 _lastMeasturedTimeInMs = _timeAtCreationInMs; 99 100 _dirtyAreasAreNotYetComputed = true; 101 102 // The drawRect: failure of having small rectangles started with 11.0 Big Sur beta 9. 103 // It was afterwards removed for Issue #705, it's no longer useful in Monterey, and in Ardour was making things worse. 104 version(OSX) 105 _drawRectWorkaround = (getMacOSVersion().major == 11); 106 107 if (!_isHostWindow) 108 { 109 DPlugCustomView.generateClassName(); 110 DPlugCustomView.registerSubclass(); 111 112 _view = DPlugCustomView.alloc(); 113 _view.initialize(this, width, height); 114 115 // GOAL: Force display by the GPU, this is supposed to solve 116 // resampling problems on HiDPI like 4k and 5k 117 // REALITY: QA reports this to be blurrier AND slower than previously 118 // Layer has to be there for the drawRect workaround. 119 if (_drawRectWorkaround) 120 _view.setWantsLayer(YES); 121 122 //_view.layer.setDrawsAsynchronously(YES); 123 // This is supposed to make things faster, but doesn't 124 //_view.layer.setOpaque(YES); 125 126 // In VST, add the view to the parent view. 127 // In AU (parentWindow == null), a reference to the view is returned instead and the host does it. 128 if (parentWindow !is null) 129 { 130 NSView parentView = NSView(cast(id)parentWindow); 131 parentView.addSubview(_view); 132 } 133 134 // See Issue #688: when changing the buffer size or sampling rate, 135 // Logic destroy and reloads the plugin, with same settings. The window 136 // is reused, thus layout doesn't get called and the plugin is unsized! 137 layout(); 138 } 139 else 140 { 141 _nsApplication = NSApplication.sharedApplication; 142 _nsApplication.setActivationPolicy(NSApplicationActivationPolicyRegular); 143 _nsWindow = NSWindow.alloc(); 144 _nsWindow.initWithContentRect(NSMakeRect(0, 0, width, height), 145 NSBorderlessWindowMask, NSBackingStoreBuffered, NO); 146 _nsWindow.makeKeyAndOrderFront(); 147 _nsApplication.activateIgnoringOtherApps(YES); 148 } 149 } 150 151 ~this() 152 { 153 if (!_isHostWindow) 154 { 155 _terminated = true; 156 157 { 158 _view.killTimer(); 159 } 160 161 _view.removeFromSuperview(); 162 _view.release(); 163 _view = DPlugCustomView(null); 164 165 DPlugCustomView.unregisterSubclass(); 166 } 167 else 168 { 169 _nsWindow.destroy(); 170 } 171 172 releaseCocoaFunctions(); 173 } 174 175 // Implements IWindow 176 override void waitEventAndDispatch() 177 { 178 if (!_isHostWindow) 179 assert(false); // only valid for a host window 180 181 NSEvent event = _nsWindow.nextEventMatchingMask(cast(NSUInteger)-1); 182 _nsApplication.sendEvent(event); 183 } 184 185 override bool terminated() 186 { 187 return _terminated; 188 } 189 190 override uint getTimeMs() 191 { 192 double timeMs = NSDate.timeIntervalSinceReferenceDate() * 1000.0; 193 194 // WARNING: ARM and x86 do not convert float to int in the same way. 195 // Convert to 64-bit and use integer truncation rather than UB. 196 // See: https://github.com/ldc-developers/ldc/issues/3603 197 long timeMs_integer = cast(long)timeMs; 198 uint ms = cast(uint)(timeMs_integer); 199 return ms; 200 } 201 202 override void* systemHandle() 203 { 204 if (_isHostWindow) 205 return _nsWindow.contentView()._id; // return the main NSView 206 else 207 return _view._id; 208 } 209 210 override bool requestResize(int widthLogicalPixels, int heightLogicalPixels, bool alsoResizeParentWindow) 211 { 212 assert(!alsoResizeParentWindow); // unsupported here 213 NSSize size = NSSize(cast(CGFloat)widthLogicalPixels, 214 cast(CGFloat)heightLogicalPixels); 215 _view.setFrameSize(size); 216 return true; 217 } 218 219 private: 220 221 MouseState getMouseState(NSEvent event) 222 { 223 // not working 224 MouseState state; 225 uint pressedMouseButtons = event.pressedMouseButtons(); 226 if (pressedMouseButtons & 1) 227 state.leftButtonDown = true; 228 if (pressedMouseButtons & 2) 229 state.rightButtonDown = true; 230 if (pressedMouseButtons & 4) 231 state.middleButtonDown = true; 232 233 NSEventModifierFlags mod = event.modifierFlags(); 234 if (mod & NSControlKeyMask) 235 state.ctrlPressed = true; 236 if (mod & NSShiftKeyMask) 237 state.shiftPressed = true; 238 if (mod & NSAlternateKeyMask) 239 state.altPressed = true; 240 241 return state; 242 } 243 244 void handleMouseWheel(NSEvent event) 245 { 246 double ddeltaX = event.deltaX; 247 double ddeltaY = event.deltaY; 248 int deltaX = 0; 249 int deltaY = 0; 250 if (ddeltaX > 0) deltaX = 1; 251 if (ddeltaX < 0) deltaX = -1; 252 if (ddeltaY > 0) deltaY = 1; 253 if (ddeltaY < 0) deltaY = -1; 254 if (deltaX || deltaY) 255 { 256 vec2i mousePos = getMouseXY(_view, event, _height); 257 _listener.onMouseWheel(mousePos.x, mousePos.y, deltaX, deltaY, getMouseState(event)); 258 } 259 } 260 261 bool handleKeyEvent(NSEvent event, bool released) 262 { 263 uint keyCode = event.keyCode(); 264 Key key; 265 switch (keyCode) 266 { 267 case kVK_ANSI_Keypad0: key = Key.digit0; break; 268 case kVK_ANSI_Keypad1: key = Key.digit1; break; 269 case kVK_ANSI_Keypad2: key = Key.digit2; break; 270 case kVK_ANSI_Keypad3: key = Key.digit3; break; 271 case kVK_ANSI_Keypad4: key = Key.digit4; break; 272 case kVK_ANSI_Keypad5: key = Key.digit5; break; 273 case kVK_ANSI_Keypad6: key = Key.digit6; break; 274 case kVK_ANSI_Keypad7: key = Key.digit7; break; 275 case kVK_ANSI_Keypad8: key = Key.digit8; break; 276 case kVK_ANSI_Keypad9: key = Key.digit9; break; 277 case kVK_Return: key = Key.enter; break; 278 case kVK_Escape: key = Key.escape; break; 279 case kVK_LeftArrow: key = Key.leftArrow; break; 280 case kVK_RightArrow: key = Key.rightArrow; break; 281 case kVK_DownArrow: key = Key.downArrow; break; 282 case kVK_UpArrow: key = Key.upArrow; break; 283 case kVK_Delete: key = Key.backspace; break; 284 case kVK_ForwardDelete: key = Key.suppr; break; 285 default: 286 { 287 NSString characters = event.charactersIgnoringModifiers(); 288 if (characters.length() == 0) 289 { 290 key = Key.unsupported; 291 } 292 else 293 { 294 wchar ch = characters.characterAtIndex(0); 295 if (ch >= '0' && ch <= '9') 296 key = cast(Key)(Key.digit0 + (ch - '0')); 297 else if (ch >= 'A' && ch <= 'Z') 298 key = cast(Key)(Key.A + (ch - 'A')); 299 else if (ch >= 'a' && ch <= 'z') 300 key = cast(Key)(Key.a + (ch - 'a')); 301 else 302 key = Key.unsupported; 303 } 304 } 305 } 306 307 bool handled = false; 308 309 if (released) 310 { 311 if (_listener.onKeyDown(key)) 312 handled = true; 313 } 314 else 315 { 316 if (_listener.onKeyUp(key)) 317 handled = true; 318 } 319 320 return handled; 321 } 322 323 void handleMouseMove(NSEvent event) 324 { 325 vec2i mousePos = getMouseXY(_view, event, _height); 326 327 if (_firstMouseMove) 328 { 329 _firstMouseMove = false; 330 _lastMouseX = mousePos.x; 331 _lastMouseY = mousePos.y; 332 } 333 334 _listener.onMouseMove(mousePos.x, mousePos.y, mousePos.x - _lastMouseX, mousePos.y - _lastMouseY, 335 getMouseState(event)); 336 337 version(noCursors) 338 {} 339 else 340 { 341 setMouseCursor(_listener.getMouseCursor()); 342 } 343 344 _lastMouseX = mousePos.x; 345 _lastMouseY = mousePos.y; 346 } 347 348 void handleMouseEntered(NSEvent event) 349 { 350 // Welcome to Issue #737. 351 // 352 // Consider the mouse cursor has changed, since the mouse was elsewhere. 353 // Give it an impossible mouse cursor cached value. 354 _lastMouseCursor = cast(MouseCursor) -1; 355 356 // Furthermore, because: 357 // 1. either the cursor might not be set by subsequent 358 // 2. either the mouseMove event might not be called, because the window is hosted in another process 359 // and isn't active yet (macOS has "click through", a sort of mouse focus) 360 // then we need to set the mouse cursor upon entry. Tricky! 361 version(noCursors) 362 { 363 setMouseCursor(MouseCursor.pointer); 364 } 365 else 366 { 367 setMouseCursor(_listener.getMouseCursor()); 368 } 369 } 370 371 void handleMouseClicks(NSEvent event, MouseButton mb, bool released) 372 { 373 vec2i mousePos = getMouseXY(_view, event, _height); 374 375 if (released) 376 _listener.onMouseRelease(mousePos.x, mousePos.y, mb, getMouseState(event)); 377 else 378 { 379 // Fix Issue #281 380 // This resets _lastMouseX and _lastMouseY on new clicks, 381 // necessary if the focus was lost for a while. 382 _firstMouseMove = true; 383 384 int clickCount = event.clickCount(); 385 bool isDoubleClick = clickCount >= 2; 386 _listener.onMouseClick(mousePos.x, mousePos.y, mb, isDoubleClick, getMouseState(event)); 387 } 388 } 389 390 void layout() 391 { 392 // Updates internal buffers in case of startup/resize 393 { 394 NSRect frameRect = _view.frame(); 395 // Note: even if the frame rect is wrong, we can support any internal size with cropping etc. 396 // TODO: is it really wrong though? 397 int width = cast(int)(frameRect.size.width); // truncating down the dimensions of bounds 398 int height = cast(int)(frameRect.size.height); 399 updateSizeIfNeeded(width, height); 400 } 401 } 402 403 void viewWillDraw() 404 { 405 if (_drawRectWorkaround) 406 { 407 CALayer layer = _view.layer(); 408 409 if (layer) 410 { 411 // On Big Sur this is technically a no-op, but that reverts the drawRect behaviour! 412 // This workaround is sanctionned by Apple: https://gist.github.com/lukaskubanek/9a61ac71dc0db8bb04db2028f2635779#gistcomment-3901461 413 layer.setContentsFormat(kCAContentsFormatRGBA8Uint); 414 } 415 } 416 } 417 418 void drawRect(NSRect rect) 419 { 420 NSGraphicsContext nsContext = NSGraphicsContext.currentContext(); 421 422 // The first drawRect callback occurs before the timer triggers. 423 // But because recomputeDirtyAreas() wasn't called before there is nothing to draw. 424 // Hence, do it. 425 if (_dirtyAreasAreNotYetComputed) 426 { 427 _dirtyAreasAreNotYetComputed = false; 428 _listener.recomputeDirtyAreas(); 429 } 430 431 _listener.onDraw(WindowPixelFormat.ARGB8); 432 433 version(useCoreGraphicsContext) 434 { 435 CGContextRef cgContext = nsContext.getCGContext(); 436 437 enum bool fullDraw = false; 438 439 //import core.stdc.stdio; 440 //printf("drawRect: _wfb WxH = %dx%d\n", _wfb.w, _wfb.h); 441 //printf(" width = %d height = %d\n", _width, _height); 442 //printf(" rect = %f %f %f %f\n", 443 // rect.origin.x, 444 // rect.origin.y, 445 // rect.origin.x+rect.size.width, 446 // rect.origin.y+rect.size.height); 447 448 // Some combinations, like Studio One 7 + CLAP, send an 449 // invalid rect first (say: -1,0 580x500 instead of 450 // 0,0 500x500). 451 // However subsequent code can deal with it. I'm warey of the 452 // optimizer removing the workaround for the condition we 453 // asserted on! 454 //assert(_wfb.w == _width); 455 //assert(_wfb.h == _height); 456 //assert(_wfb.w >= cast(int)(rect.origin.x+rect.size.width)); 457 //assert(_wfb.h >= cast(int)(rect.origin.y+rect.size.height)); 458 459 static if (fullDraw) 460 { 461 size_t sizeNeeded = _wfb.pitch * _wfb.h; 462 size_t bytesPerRow = _wfb.pitch; 463 464 CGDataProviderRef provider = CGDataProviderCreateWithData(null, _wfb.pixels, sizeNeeded, null); 465 CGImageRef image = CGImageCreate(_width, 466 _height, 467 8, 468 32, 469 bytesPerRow, 470 _cgColorSpaceRef, 471 kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst, 472 provider, 473 null, 474 true, 475 kCGRenderingIntentDefault); 476 // "on return, you may safely release [the provider]" 477 CGDataProviderRelease(provider); 478 scope(exit) CGImageRelease(image); 479 CGRect fullRect = CGMakeRect(0, 0, _width, _height); 480 CGContextDrawImage(cgContext, fullRect, image); 481 } 482 else 483 { 484 /// rect can be outside frame and needs clipping. 485 /// 486 /// "Some patterns that have historically worked will require adjustment: 487 /// Filling the dirty rect of a view inside of -drawRect. A fairly common 488 /// pattern is to simply rect fill the dirty rect passed into an override 489 /// of NSView.draw(). The dirty rect can now extend outside of your view's 490 /// bounds. This pattern can be adjusted by filling the bounds instead of 491 /// the dirty rect, or by setting clipsToBounds = true. 492 /// Confusing a view’s bounds and its dirty rect. The dirty rect passed to .drawRect() 493 /// should be used to determine what to draw, not where to draw it. Use NSView.bounds 494 /// when determining the layout of what your view draws." (10905750) 495 /// 496 /// Thus is the story of Issue #835 (and afterwards, Issue #885) 497 498 int rectOrigX = cast(int)rect.origin.x; 499 int rectOrigY = cast(int)rect.origin.y; 500 int rectWidth = cast(int)rect.size.width; 501 int rectHeight = cast(int)rect.size.height; 502 503 box2i dirtyRect = box2i.rectangle(rectOrigX, rectOrigY, rectWidth, rectHeight); 504 box2i bounds = box2i(0, 0, _width, _height); 505 506 // clip dirtyRect to bounds 507 // it CAN be made empty with energetic resizing, the 508 // base offset might already be outside a window that 509 // is shrinking 510 box2i clipped = dirtyRect.intersection(bounds); 511 if (!clipped.empty) 512 { 513 int clippedOrigX = clipped.min.x; 514 int clippedOrigY = clipped.min.y; 515 int clippedWidth = clipped.width; 516 int clippedHeight = clipped.height; 517 518 int ysource = -clippedOrigY + _height - clippedHeight; 519 520 assert(ysource >= 0); 521 assert(ysource < _height); 522 523 const (RGBA)* firstPixel = &(_wfb.scanline(ysource)[clippedOrigX]); 524 size_t sizeNeeded = _wfb.pitch * clippedHeight; 525 size_t bytesPerRow = _wfb.pitch; 526 527 CGDataProviderRef provider = CGDataProviderCreateWithData(null, firstPixel, sizeNeeded, null); 528 529 CGImageRef image = CGImageCreate(clippedWidth, 530 clippedHeight, 531 8, 532 32, 533 bytesPerRow, 534 _cgColorSpaceRef, 535 kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst, 536 provider, 537 null, 538 true, 539 kCGRenderingIntentDefault); 540 // "on return, you may safely release [the provider]" 541 CGDataProviderRelease(provider); 542 scope(exit) CGImageRelease(image); 543 544 CGRect clippedDirtyRect = CGMakeRect(clippedOrigX, clippedOrigY, clippedWidth, clippedHeight); 545 CGContextDrawImage(cgContext, clippedDirtyRect, image); 546 } 547 } 548 } 549 else 550 { 551 size_t sizeNeeded = _wfb.pitch * _wfb.h; 552 size_t bytesPerRow = _wfb.pitch; 553 CIContext ciContext = nsContext.getCIContext(); 554 _imageData = NSData.dataWithBytesNoCopy(_wfb.pixels, sizeNeeded, false); 555 556 CIImage image = CIImage.imageWithBitmapData(_imageData, 557 bytesPerRow, 558 CGSize(_width, _height), 559 kCIFormatARGB8, 560 _cgColorSpaceRef); 561 ciContext.drawImage(image, rect, rect); 562 } 563 } 564 565 /// Returns: true if window size changed. 566 bool updateSizeIfNeeded(int newWidth, int newHeight) 567 { 568 // only do something if the client size has changed 569 if ( (newWidth != _width) || (newHeight != _height) ) 570 { 571 _width = newWidth; 572 _height = newHeight; 573 _wfb = _listener.onResized(_width, _height); 574 return true; 575 } 576 else 577 return false; 578 } 579 580 void doAnimation() 581 { 582 uint now = getTimeMs(); 583 double dt = (now - _lastMeasturedTimeInMs) * 0.001; 584 double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days 585 _lastMeasturedTimeInMs = now; 586 _listener.onAnimate(dt, time); 587 } 588 589 void onTimer() 590 { 591 // Deal with animation 592 doAnimation(); 593 594 _listener.recomputeDirtyAreas(); 595 _dirtyAreasAreNotYetComputed = false; 596 box2i dirtyRect = _listener.getDirtyRectangle(); 597 if (!dirtyRect.empty()) 598 { 599 600 NSRect boundsRect = _view.bounds(); 601 int height = cast(int)(boundsRect.size.height); 602 NSRect r = NSMakeRect(dirtyRect.min.x, 603 height - dirtyRect.min.y - dirtyRect.height, 604 dirtyRect.width, 605 dirtyRect.height); 606 _view.setNeedsDisplayInRect(r); 607 } 608 } 609 610 void setMouseCursor(MouseCursor dplugCursor) 611 { 612 if(dplugCursor != _lastMouseCursor) 613 { 614 if(dplugCursor == MouseCursor.hidden) 615 { 616 NSCursor.hide(); 617 } 618 else 619 { 620 if(_lastMouseCursor == MouseCursor.hidden) 621 { 622 NSCursor.unhide(); 623 } 624 625 NSCursor.pop(); 626 NSCursor nsCursor; 627 switch(dplugCursor) 628 { 629 case MouseCursor.linkSelect: 630 nsCursor = NSCursor.pointingHandCursor(); 631 break; 632 case MouseCursor.drag: 633 nsCursor = NSCursor.crosshairCursor(); 634 break; 635 case MouseCursor.move: 636 nsCursor = NSCursor.openHandCursor(); 637 break; 638 case MouseCursor.verticalResize: 639 nsCursor = NSCursor.resizeUpDownCursor(); 640 break; 641 case MouseCursor.horizontalResize: 642 nsCursor = NSCursor.resizeLeftRightCursor(); 643 break; 644 case MouseCursor.diagonalResize: 645 nsCursor = NSCursor.crosshairCursor(); // macOS doesn't seem to have this 646 break; 647 case MouseCursor.pointer: 648 default: 649 nsCursor = NSCursor.arrowCursor(); 650 } 651 nsCursor.push(); 652 } 653 654 _lastMouseCursor = dplugCursor; 655 } 656 } 657 } 658 659 struct DPlugCustomView 660 { 661 public: 662 nothrow: 663 @nogc: 664 665 NSView parent; 666 alias parent this; 667 668 // create from an id 669 this (id id_) 670 { 671 this._id = id_; 672 } 673 674 /// Allocates, but do not init 675 static DPlugCustomView alloc() 676 { 677 alias fun_t = extern(C) id function (id obj, SEL sel) nothrow @nogc; 678 return DPlugCustomView( (cast(fun_t)objc_msgSend)(getClassID(), sel!"alloc") ); 679 } 680 681 static Class getClass() 682 { 683 return cast(Class)( getClassID() ); 684 } 685 686 static id getClassID() 687 { 688 return objc_getClass(customClassName.ptr); 689 } 690 691 // This class uses a unique class name for each plugin instance 692 static __gshared char[16 + 36 + 1] customClassName; 693 694 static void generateClassName() nothrow @nogc 695 { 696 generateNullTerminatedRandomUUID!char(customClassName, "DPlugCustomView_"); 697 } 698 699 private: 700 701 CocoaWindow _window; 702 NSTimer _timer = null; 703 NSString _runLoopMode; 704 NSTrackingArea _trackingArea; 705 706 void initialize(CocoaWindow window, int width, int height) 707 { 708 // Warning: taking this address is fishy since DPlugCustomView is a struct and thus could be copied 709 // we rely on the fact it won't :| 710 void* thisPointer = cast(void*)(&this); 711 object_setInstanceVariable(_id, "this", thisPointer); 712 713 this._window = window; 714 715 NSRect r = NSRect(NSPoint(0, 0), NSSize(width, height)); 716 initWithFrame(r); 717 718 _timer = NSTimer.timerWithTimeInterval(1 / 60.0, this, sel!"onTimer:", null, true); 719 _runLoopMode = NSString.stringWith("kCFRunLoopCommonModes"w); 720 NSRunLoop.currentRunLoop().addTimer(_timer, _runLoopMode); 721 } 722 723 static __gshared Class clazz; 724 725 static void registerSubclass() 726 { 727 clazz = objc_allocateClassPair(cast(Class) lazyClass!"NSView", customClassName.ptr, 0); 728 729 class_addMethod(clazz, sel!"keyDown:", cast(IMP) &keyDown, "v@:@"); 730 class_addMethod(clazz, sel!"keyUp:", cast(IMP) &keyUp, "v@:@"); 731 class_addMethod(clazz, sel!"mouseDown:", cast(IMP) &mouseDown, "v@:@"); 732 class_addMethod(clazz, sel!"mouseUp:", cast(IMP) &mouseUp, "v@:@"); 733 class_addMethod(clazz, sel!"rightMouseDown:", cast(IMP) &rightMouseDown, "v@:@"); 734 class_addMethod(clazz, sel!"rightMouseUp:", cast(IMP) &rightMouseUp, "v@:@"); 735 class_addMethod(clazz, sel!"otherMouseDown:", cast(IMP) &otherMouseDown, "v@:@"); 736 class_addMethod(clazz, sel!"otherMouseUp:", cast(IMP) &otherMouseUp, "v@:@"); 737 class_addMethod(clazz, sel!"mouseMoved:", cast(IMP) &mouseMoved, "v@:@"); 738 class_addMethod(clazz, sel!"mouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 739 class_addMethod(clazz, sel!"rightMouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 740 class_addMethod(clazz, sel!"otherMouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 741 class_addMethod(clazz, sel!"acceptsFirstResponder", cast(IMP) &acceptsFirstResponder, "b@:"); 742 class_addMethod(clazz, sel!"isOpaque", cast(IMP) &isOpaque, "b@:"); 743 class_addMethod(clazz, sel!"acceptsFirstMouse:", cast(IMP) &acceptsFirstMouse, "b@:@"); 744 class_addMethod(clazz, sel!"viewDidMoveToWindow", cast(IMP) &viewDidMoveToWindow, "v@:"); 745 class_addMethod(clazz, sel!"layout", cast(IMP) &layout, "v@:"); 746 class_addMethod(clazz, sel!"drawRect:", cast(IMP) &drawRect, "v@:" ~ encode!NSRect); 747 class_addMethod(clazz, sel!"onTimer:", cast(IMP) &onTimer, "v@:@"); 748 class_addMethod(clazz, sel!"viewWillDraw", cast(IMP) &viewWillDraw, "v@:"); 749 750 class_addMethod(clazz, sel!"mouseEntered:", cast(IMP) &mouseEntered, "v@:@"); 751 class_addMethod(clazz, sel!"mouseExited:", cast(IMP) &mouseExited, "v@:@"); 752 class_addMethod(clazz, sel!"updateTrackingAreas", cast(IMP)&updateTrackingAreas, "v@:"); 753 754 // This ~ is to avoid a strange DMD ICE. Didn't succeed in isolating it. 755 class_addMethod(clazz, sel!("scroll" ~ "Wheel:") , cast(IMP) &scrollWheel, "v@:@"); 756 757 // very important: add an instance variable for the this pointer so that the D object can be 758 // retrieved from an id 759 class_addIvar(clazz, "this", (void*).sizeof, (void*).sizeof == 4 ? 2 : 3, "^v"); 760 761 objc_registerClassPair(clazz); 762 } 763 764 static void unregisterSubclass() 765 { 766 // For some reason the class need to continue to exist, so we leak it 767 // objc_disposeClassPair(clazz); 768 // TODO: remove this crap 769 } 770 771 void killTimer() 772 { 773 if (_timer) 774 { 775 _timer.invalidate(); 776 _timer = NSTimer(null); 777 } 778 } 779 } 780 781 DPlugCustomView getInstance(id anId) nothrow @nogc 782 { 783 // strange thing: object_getInstanceVariable definition is odd (void**) 784 // and only works for pointer-sized values says SO 785 void* thisPointer = null; 786 Ivar var = object_getInstanceVariable(anId, "this", &thisPointer); 787 assert(var !is null); 788 assert(thisPointer !is null); 789 return *cast(DPlugCustomView*)thisPointer; 790 } 791 792 vec2i getMouseXY(NSView view, NSEvent event, int windowHeight) nothrow @nogc 793 { 794 NSPoint mouseLocation = event.locationInWindow(); 795 mouseLocation = view.convertPoint(mouseLocation, NSView(null)); 796 int px = cast(int)(mouseLocation.x) - 2; 797 int py = windowHeight - cast(int)(mouseLocation.y) - 3; 798 return vec2i(px, py); 799 } 800 801 802 803 alias CocoaScopedCallback = ScopedForeignCallback!(true, true); 804 805 // Overridden function gets called with an id, instead of the self pointer. 806 // So we have to get back the D class object address. 807 // Big thanks to Mike Ash (@macdev) 808 // MAYDO: why are these methods members??? 809 extern(C) 810 { 811 void keyDown(id self, SEL selector, id event) nothrow @nogc 812 { 813 CocoaScopedCallback scopedCallback; 814 scopedCallback.enter(); 815 816 DPlugCustomView view = getInstance(self); 817 bool handled = view._window.handleKeyEvent(NSEvent(event), false); 818 819 // send event to superclass if event not handled 820 if (!handled) 821 { 822 objc_super sup; 823 sup.receiver = self; 824 sup.clazz = cast(Class) lazyClass!"NSView"; 825 alias fun_t = extern(C) void function (objc_super*, SEL, id) nothrow @nogc; 826 (cast(fun_t)objc_msgSendSuper)(&sup, selector, event); 827 } 828 } 829 830 void keyUp(id self, SEL selector, id event) nothrow @nogc 831 { 832 CocoaScopedCallback scopedCallback; 833 scopedCallback.enter(); 834 835 DPlugCustomView view = getInstance(self); 836 view._window.handleKeyEvent(NSEvent(event), true); 837 } 838 839 void mouseDown(id self, SEL selector, id event) nothrow @nogc 840 { 841 CocoaScopedCallback scopedCallback; 842 scopedCallback.enter(); 843 844 DPlugCustomView view = getInstance(self); 845 view._window.handleMouseClicks(NSEvent(event), MouseButton.left, false); 846 } 847 848 void mouseUp(id self, SEL selector, id event) nothrow @nogc 849 { 850 CocoaScopedCallback scopedCallback; 851 scopedCallback.enter(); 852 853 DPlugCustomView view = getInstance(self); 854 view._window.handleMouseClicks(NSEvent(event), MouseButton.left, true); 855 } 856 857 void rightMouseDown(id self, SEL selector, id event) nothrow @nogc 858 { 859 CocoaScopedCallback scopedCallback; 860 scopedCallback.enter(); 861 862 DPlugCustomView view = getInstance(self); 863 view._window.handleMouseClicks(NSEvent(event), MouseButton.right, false); 864 } 865 866 void rightMouseUp(id self, SEL selector, id event) nothrow @nogc 867 { 868 CocoaScopedCallback scopedCallback; 869 scopedCallback.enter(); 870 871 DPlugCustomView view = getInstance(self); 872 view._window.handleMouseClicks(NSEvent(event), MouseButton.right, true); 873 } 874 875 void otherMouseDown(id self, SEL selector, id event) nothrow @nogc 876 { 877 CocoaScopedCallback scopedCallback; 878 scopedCallback.enter(); 879 880 DPlugCustomView view = getInstance(self); 881 auto nsEvent = NSEvent(event); 882 if (nsEvent.buttonNumber == 2) 883 view._window.handleMouseClicks(nsEvent, MouseButton.middle, false); 884 } 885 886 void otherMouseUp(id self, SEL selector, id event) nothrow @nogc 887 { 888 CocoaScopedCallback scopedCallback; 889 scopedCallback.enter(); 890 891 DPlugCustomView view = getInstance(self); 892 auto nsEvent = NSEvent(event); 893 if (nsEvent.buttonNumber == 2) 894 view._window.handleMouseClicks(nsEvent, MouseButton.middle, true); 895 } 896 897 void mouseMoved(id self, SEL selector, id event) nothrow @nogc 898 { 899 CocoaScopedCallback scopedCallback; 900 scopedCallback.enter(); 901 902 DPlugCustomView view = getInstance(self); 903 view._window.handleMouseMove(NSEvent(event)); 904 } 905 906 void mouseEntered(id self, SEL selector, id event) nothrow @nogc 907 { 908 CocoaScopedCallback scopedCallback; 909 scopedCallback.enter(); 910 DPlugCustomView view = getInstance(self); 911 view._window.handleMouseEntered(NSEvent(event)); 912 } 913 914 void mouseExited(id self, SEL selector, id event) nothrow @nogc 915 { 916 CocoaScopedCallback scopedCallback; 917 scopedCallback.enter(); 918 DPlugCustomView view = getInstance(self); 919 view._window._listener.onMouseExitedWindow(); 920 } 921 922 void updateTrackingAreas(id self, SEL selector) nothrow @nogc 923 { 924 CocoaScopedCallback scopedCallback; 925 scopedCallback.enter(); 926 927 // Call superclass's updateTrackingAreas:, equivalent to [super updateTrackingAreas]; 928 { 929 objc_super sup; 930 sup.receiver = self; 931 sup.clazz = cast(Class) lazyClass!"NSView"; 932 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 933 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 934 } 935 936 DPlugCustomView view = getInstance(self); 937 938 // Remove an existing tracking area, if any. 939 if (view._trackingArea._id !is null) 940 { 941 view.removeTrackingArea(view._trackingArea); 942 view._trackingArea.release(); 943 view._trackingArea._id = null; 944 } 945 946 // This is needed to get mouseEntered and mouseExited 947 int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways); 948 949 NSRect bounds = view.bounds(); 950 view._trackingArea = NSTrackingArea.alloc(); 951 view._trackingArea.initWithRect(bounds, opts, view, null); 952 view.addTrackingArea(view._trackingArea); 953 } 954 955 956 void scrollWheel(id self, SEL selector, id event) nothrow @nogc 957 { 958 CocoaScopedCallback scopedCallback; 959 scopedCallback.enter(); 960 DPlugCustomView view = getInstance(self); 961 view._window.handleMouseWheel(NSEvent(event)); 962 } 963 964 bool acceptsFirstResponder(id self, SEL selector) nothrow @nogc 965 { 966 return YES; 967 } 968 969 bool acceptsFirstMouse(id self, SEL selector, id pEvent) nothrow @nogc 970 { 971 return YES; 972 } 973 974 bool isOpaque(id self, SEL selector) nothrow @nogc 975 { 976 return NO; // Since with the #835 issue, doesn't cover all the dirt rect but only the part intersecting bounds. 977 } 978 979 // Since 10.7, called on resize. 980 void layout(id self, SEL selector) nothrow @nogc 981 { 982 CocoaScopedCallback scopedCallback; 983 scopedCallback.enter(); 984 985 DPlugCustomView view = getInstance(self); 986 view._window.layout(); 987 988 // Call superclass's layout:, equivalent to [super layout]; 989 { 990 objc_super sup; 991 sup.receiver = self; 992 sup.clazz = cast(Class) lazyClass!"NSView"; 993 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 994 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 995 } 996 } 997 998 // Necessary for the Big Sur drawRect: fuckup 999 // See Issue #505. 1000 void viewWillDraw(id self, SEL selector) nothrow @nogc 1001 { 1002 CocoaScopedCallback scopedCallback; 1003 scopedCallback.enter(); 1004 1005 DPlugCustomView view = getInstance(self); 1006 view._window.viewWillDraw(); 1007 1008 // Call superclass's layout:, equivalent to [super viewWillDraw]; 1009 { 1010 objc_super sup; 1011 sup.receiver = self; 1012 sup.clazz = cast(Class) lazyClass!"NSView"; 1013 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 1014 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 1015 } 1016 } 1017 1018 void viewDidMoveToWindow(id self, SEL selector) nothrow @nogc 1019 { 1020 CocoaScopedCallback scopedCallback; 1021 scopedCallback.enter(); 1022 1023 DPlugCustomView view = getInstance(self); 1024 NSWindow parentWindow = view.window(); 1025 if (parentWindow) 1026 { 1027 parentWindow.makeFirstResponder(view); 1028 parentWindow.setAcceptsMouseMovedEvents(true); 1029 } 1030 } 1031 1032 void drawRect(id self, SEL selector, NSRect rect) nothrow @nogc 1033 { 1034 CocoaScopedCallback scopedCallback; 1035 scopedCallback.enter(); 1036 1037 DPlugCustomView view = getInstance(self); 1038 view._window.drawRect(rect); 1039 } 1040 1041 void onTimer(id self, SEL selector, id timer) nothrow @nogc 1042 { 1043 CocoaScopedCallback scopedCallback; 1044 scopedCallback.enter(); 1045 1046 DPlugCustomView view = getInstance(self); 1047 view._window.onTimer(); 1048 } 1049 }