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 // Would be very interesting to find a crash there! Please contact Dplug admins if you do so. 449 assert(_wfb.w == _width); 450 assert(_wfb.h == _height); 451 assert(_wfb.w >= cast(int)(rect.origin.x+rect.size.width)); 452 assert(_wfb.h >= cast(int)(rect.origin.y+rect.size.height)); 453 454 static if (fullDraw) 455 { 456 size_t sizeNeeded = _wfb.pitch * _wfb.h; 457 size_t bytesPerRow = _wfb.pitch; 458 459 CGDataProviderRef provider = CGDataProviderCreateWithData(null, _wfb.pixels, sizeNeeded, null); 460 CGImageRef image = CGImageCreate(_width, 461 _height, 462 8, 463 32, 464 bytesPerRow, 465 _cgColorSpaceRef, 466 kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst, 467 provider, 468 null, 469 true, 470 kCGRenderingIntentDefault); 471 // "on return, you may safely release [the provider]" 472 CGDataProviderRelease(provider); 473 scope(exit) CGImageRelease(image); 474 475 CGRect fullRect = CGMakeRect(0, 0, _width, _height); 476 CGContextDrawImage(cgContext, fullRect, image); 477 } 478 else 479 { 480 /// rect can be outsides frame and needs clipping. 481 /// 482 /// "Some patterns that have historically worked will require adjustment: 483 /// Filling the dirty rect of a view inside of -drawRect. A fairly common 484 /// pattern is to simply rect fill the dirty rect passed into an override 485 /// of NSView.draw(). The dirty rect can now extend outside of your view's 486 /// bounds. This pattern can be adjusted by filling the bounds instead of 487 /// the dirty rect, or by setting clipsToBounds = true. 488 /// Confusing a view’s bounds and its dirty rect. The dirty rect passed to .drawRect() 489 /// should be used to determine what to draw, not where to draw it. Use NSView.bounds 490 /// when determining the layout of what your view draws." (10905750) 491 /// 492 /// Thus is the story of Issue #835. 493 494 int rectOrigX = cast(int)rect.origin.x; 495 int rectOrigY = cast(int)rect.origin.y; 496 int rectWidth = cast(int)rect.size.width; 497 int rectHeight = cast(int)rect.size.height; 498 499 box2i dirtyRect = box2i.rectangle(rectOrigX, rectOrigY, rectWidth, rectHeight); 500 box2i bounds = box2i(0, 0, _width, _height); 501 502 // clip dirtyRect to bounds 503 box2i clipped = dirtyRect.intersection(bounds); 504 505 506 int clippedOrigX = clipped.min.x; 507 int clippedOrigY = clipped.min.y; 508 int clippedWidth = clipped.width; 509 int clippedHeight = clipped.height; 510 511 int ysource = -clippedOrigY + _height - clippedHeight; 512 513 assert(ysource >= 0); 514 assert(ysource < _height); 515 516 const (RGBA)* firstPixel = &(_wfb.scanline(ysource)[clippedOrigX]); 517 size_t sizeNeeded = _wfb.pitch * clippedHeight; 518 size_t bytesPerRow = _wfb.pitch; 519 520 CGDataProviderRef provider = CGDataProviderCreateWithData(null, firstPixel, sizeNeeded, null); 521 522 CGImageRef image = CGImageCreate(clippedWidth, 523 clippedHeight, 524 8, 525 32, 526 bytesPerRow, 527 _cgColorSpaceRef, 528 kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst, 529 provider, 530 null, 531 true, 532 kCGRenderingIntentDefault); 533 // "on return, you may safely release [the provider]" 534 CGDataProviderRelease(provider); 535 scope(exit) CGImageRelease(image); 536 537 CGRect clippedDirtyRect = CGMakeRect(clippedOrigX, clippedOrigY, clippedWidth, clippedHeight); 538 CGContextDrawImage(cgContext, clippedDirtyRect, image); 539 } 540 } 541 else 542 { 543 size_t sizeNeeded = _wfb.pitch * _wfb.h; 544 size_t bytesPerRow = _wfb.pitch; 545 CIContext ciContext = nsContext.getCIContext(); 546 _imageData = NSData.dataWithBytesNoCopy(_wfb.pixels, sizeNeeded, false); 547 548 CIImage image = CIImage.imageWithBitmapData(_imageData, 549 bytesPerRow, 550 CGSize(_width, _height), 551 kCIFormatARGB8, 552 _cgColorSpaceRef); 553 ciContext.drawImage(image, rect, rect); 554 } 555 } 556 557 /// Returns: true if window size changed. 558 bool updateSizeIfNeeded(int newWidth, int newHeight) 559 { 560 // only do something if the client size has changed 561 if ( (newWidth != _width) || (newHeight != _height) ) 562 { 563 _width = newWidth; 564 _height = newHeight; 565 _wfb = _listener.onResized(_width, _height); 566 return true; 567 } 568 else 569 return false; 570 } 571 572 void doAnimation() 573 { 574 uint now = getTimeMs(); 575 double dt = (now - _lastMeasturedTimeInMs) * 0.001; 576 double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days 577 _lastMeasturedTimeInMs = now; 578 _listener.onAnimate(dt, time); 579 } 580 581 void onTimer() 582 { 583 // Deal with animation 584 doAnimation(); 585 586 _listener.recomputeDirtyAreas(); 587 _dirtyAreasAreNotYetComputed = false; 588 box2i dirtyRect = _listener.getDirtyRectangle(); 589 if (!dirtyRect.empty()) 590 { 591 592 NSRect boundsRect = _view.bounds(); 593 int height = cast(int)(boundsRect.size.height); 594 NSRect r = NSMakeRect(dirtyRect.min.x, 595 height - dirtyRect.min.y - dirtyRect.height, 596 dirtyRect.width, 597 dirtyRect.height); 598 _view.setNeedsDisplayInRect(r); 599 } 600 } 601 602 void setMouseCursor(MouseCursor dplugCursor) 603 { 604 if(dplugCursor != _lastMouseCursor) 605 { 606 if(dplugCursor == MouseCursor.hidden) 607 { 608 NSCursor.hide(); 609 } 610 else 611 { 612 if(_lastMouseCursor == MouseCursor.hidden) 613 { 614 NSCursor.unhide(); 615 } 616 617 NSCursor.pop(); 618 NSCursor nsCursor; 619 switch(dplugCursor) 620 { 621 case MouseCursor.linkSelect: 622 nsCursor = NSCursor.pointingHandCursor(); 623 break; 624 case MouseCursor.drag: 625 nsCursor = NSCursor.crosshairCursor(); 626 break; 627 case MouseCursor.move: 628 nsCursor = NSCursor.openHandCursor(); 629 break; 630 case MouseCursor.verticalResize: 631 nsCursor = NSCursor.resizeUpDownCursor(); 632 break; 633 case MouseCursor.horizontalResize: 634 nsCursor = NSCursor.resizeLeftRightCursor(); 635 break; 636 case MouseCursor.diagonalResize: 637 nsCursor = NSCursor.crosshairCursor(); // macOS doesn't seem to have this 638 break; 639 case MouseCursor.pointer: 640 default: 641 nsCursor = NSCursor.arrowCursor(); 642 } 643 nsCursor.push(); 644 } 645 646 _lastMouseCursor = dplugCursor; 647 } 648 } 649 } 650 651 struct DPlugCustomView 652 { 653 public: 654 nothrow: 655 @nogc: 656 657 NSView parent; 658 alias parent this; 659 660 // create from an id 661 this (id id_) 662 { 663 this._id = id_; 664 } 665 666 /// Allocates, but do not init 667 static DPlugCustomView alloc() 668 { 669 alias fun_t = extern(C) id function (id obj, SEL sel) nothrow @nogc; 670 return DPlugCustomView( (cast(fun_t)objc_msgSend)(getClassID(), sel!"alloc") ); 671 } 672 673 static Class getClass() 674 { 675 return cast(Class)( getClassID() ); 676 } 677 678 static id getClassID() 679 { 680 return objc_getClass(customClassName.ptr); 681 } 682 683 // This class uses a unique class name for each plugin instance 684 static __gshared char[16 + 36 + 1] customClassName; 685 686 static void generateClassName() nothrow @nogc 687 { 688 generateNullTerminatedRandomUUID!char(customClassName, "DPlugCustomView_"); 689 } 690 691 private: 692 693 CocoaWindow _window; 694 NSTimer _timer = null; 695 NSString _runLoopMode; 696 NSTrackingArea _trackingArea; 697 698 void initialize(CocoaWindow window, int width, int height) 699 { 700 // Warning: taking this address is fishy since DPlugCustomView is a struct and thus could be copied 701 // we rely on the fact it won't :| 702 void* thisPointer = cast(void*)(&this); 703 object_setInstanceVariable(_id, "this", thisPointer); 704 705 this._window = window; 706 707 NSRect r = NSRect(NSPoint(0, 0), NSSize(width, height)); 708 initWithFrame(r); 709 710 _timer = NSTimer.timerWithTimeInterval(1 / 60.0, this, sel!"onTimer:", null, true); 711 _runLoopMode = NSString.stringWith("kCFRunLoopCommonModes"w); 712 NSRunLoop.currentRunLoop().addTimer(_timer, _runLoopMode); 713 } 714 715 static __gshared Class clazz; 716 717 static void registerSubclass() 718 { 719 clazz = objc_allocateClassPair(cast(Class) lazyClass!"NSView", customClassName.ptr, 0); 720 721 class_addMethod(clazz, sel!"keyDown:", cast(IMP) &keyDown, "v@:@"); 722 class_addMethod(clazz, sel!"keyUp:", cast(IMP) &keyUp, "v@:@"); 723 class_addMethod(clazz, sel!"mouseDown:", cast(IMP) &mouseDown, "v@:@"); 724 class_addMethod(clazz, sel!"mouseUp:", cast(IMP) &mouseUp, "v@:@"); 725 class_addMethod(clazz, sel!"rightMouseDown:", cast(IMP) &rightMouseDown, "v@:@"); 726 class_addMethod(clazz, sel!"rightMouseUp:", cast(IMP) &rightMouseUp, "v@:@"); 727 class_addMethod(clazz, sel!"otherMouseDown:", cast(IMP) &otherMouseDown, "v@:@"); 728 class_addMethod(clazz, sel!"otherMouseUp:", cast(IMP) &otherMouseUp, "v@:@"); 729 class_addMethod(clazz, sel!"mouseMoved:", cast(IMP) &mouseMoved, "v@:@"); 730 class_addMethod(clazz, sel!"mouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 731 class_addMethod(clazz, sel!"rightMouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 732 class_addMethod(clazz, sel!"otherMouseDragged:", cast(IMP) &mouseMoved, "v@:@"); 733 class_addMethod(clazz, sel!"acceptsFirstResponder", cast(IMP) &acceptsFirstResponder, "b@:"); 734 class_addMethod(clazz, sel!"isOpaque", cast(IMP) &isOpaque, "b@:"); 735 class_addMethod(clazz, sel!"acceptsFirstMouse:", cast(IMP) &acceptsFirstMouse, "b@:@"); 736 class_addMethod(clazz, sel!"viewDidMoveToWindow", cast(IMP) &viewDidMoveToWindow, "v@:"); 737 class_addMethod(clazz, sel!"layout", cast(IMP) &layout, "v@:"); 738 class_addMethod(clazz, sel!"drawRect:", cast(IMP) &drawRect, "v@:" ~ encode!NSRect); 739 class_addMethod(clazz, sel!"onTimer:", cast(IMP) &onTimer, "v@:@"); 740 class_addMethod(clazz, sel!"viewWillDraw", cast(IMP) &viewWillDraw, "v@:"); 741 742 class_addMethod(clazz, sel!"mouseEntered:", cast(IMP) &mouseEntered, "v@:@"); 743 class_addMethod(clazz, sel!"mouseExited:", cast(IMP) &mouseExited, "v@:@"); 744 class_addMethod(clazz, sel!"updateTrackingAreas", cast(IMP)&updateTrackingAreas, "v@:"); 745 746 // This ~ is to avoid a strange DMD ICE. Didn't succeed in isolating it. 747 class_addMethod(clazz, sel!("scroll" ~ "Wheel:") , cast(IMP) &scrollWheel, "v@:@"); 748 749 // very important: add an instance variable for the this pointer so that the D object can be 750 // retrieved from an id 751 class_addIvar(clazz, "this", (void*).sizeof, (void*).sizeof == 4 ? 2 : 3, "^v"); 752 753 objc_registerClassPair(clazz); 754 } 755 756 static void unregisterSubclass() 757 { 758 // For some reason the class need to continue to exist, so we leak it 759 // objc_disposeClassPair(clazz); 760 // TODO: remove this crap 761 } 762 763 void killTimer() 764 { 765 if (_timer) 766 { 767 _timer.invalidate(); 768 _timer = NSTimer(null); 769 } 770 } 771 } 772 773 DPlugCustomView getInstance(id anId) nothrow @nogc 774 { 775 // strange thing: object_getInstanceVariable definition is odd (void**) 776 // and only works for pointer-sized values says SO 777 void* thisPointer = null; 778 Ivar var = object_getInstanceVariable(anId, "this", &thisPointer); 779 assert(var !is null); 780 assert(thisPointer !is null); 781 return *cast(DPlugCustomView*)thisPointer; 782 } 783 784 vec2i getMouseXY(NSView view, NSEvent event, int windowHeight) nothrow @nogc 785 { 786 NSPoint mouseLocation = event.locationInWindow(); 787 mouseLocation = view.convertPoint(mouseLocation, NSView(null)); 788 int px = cast(int)(mouseLocation.x) - 2; 789 int py = windowHeight - cast(int)(mouseLocation.y) - 3; 790 return vec2i(px, py); 791 } 792 793 794 795 alias CocoaScopedCallback = ScopedForeignCallback!(true, true); 796 797 // Overridden function gets called with an id, instead of the self pointer. 798 // So we have to get back the D class object address. 799 // Big thanks to Mike Ash (@macdev) 800 // MAYDO: why are these methods members??? 801 extern(C) 802 { 803 void keyDown(id self, SEL selector, id event) nothrow @nogc 804 { 805 CocoaScopedCallback scopedCallback; 806 scopedCallback.enter(); 807 808 DPlugCustomView view = getInstance(self); 809 bool handled = view._window.handleKeyEvent(NSEvent(event), false); 810 811 // send event to superclass if event not handled 812 if (!handled) 813 { 814 objc_super sup; 815 sup.receiver = self; 816 sup.clazz = cast(Class) lazyClass!"NSView"; 817 alias fun_t = extern(C) void function (objc_super*, SEL, id) nothrow @nogc; 818 (cast(fun_t)objc_msgSendSuper)(&sup, selector, event); 819 } 820 } 821 822 void keyUp(id self, SEL selector, id event) nothrow @nogc 823 { 824 CocoaScopedCallback scopedCallback; 825 scopedCallback.enter(); 826 827 DPlugCustomView view = getInstance(self); 828 view._window.handleKeyEvent(NSEvent(event), true); 829 } 830 831 void mouseDown(id self, SEL selector, id event) nothrow @nogc 832 { 833 CocoaScopedCallback scopedCallback; 834 scopedCallback.enter(); 835 836 DPlugCustomView view = getInstance(self); 837 view._window.handleMouseClicks(NSEvent(event), MouseButton.left, false); 838 } 839 840 void mouseUp(id self, SEL selector, id event) nothrow @nogc 841 { 842 CocoaScopedCallback scopedCallback; 843 scopedCallback.enter(); 844 845 DPlugCustomView view = getInstance(self); 846 view._window.handleMouseClicks(NSEvent(event), MouseButton.left, true); 847 } 848 849 void rightMouseDown(id self, SEL selector, id event) nothrow @nogc 850 { 851 CocoaScopedCallback scopedCallback; 852 scopedCallback.enter(); 853 854 DPlugCustomView view = getInstance(self); 855 view._window.handleMouseClicks(NSEvent(event), MouseButton.right, false); 856 } 857 858 void rightMouseUp(id self, SEL selector, id event) nothrow @nogc 859 { 860 CocoaScopedCallback scopedCallback; 861 scopedCallback.enter(); 862 863 DPlugCustomView view = getInstance(self); 864 view._window.handleMouseClicks(NSEvent(event), MouseButton.right, true); 865 } 866 867 void otherMouseDown(id self, SEL selector, id event) nothrow @nogc 868 { 869 CocoaScopedCallback scopedCallback; 870 scopedCallback.enter(); 871 872 DPlugCustomView view = getInstance(self); 873 auto nsEvent = NSEvent(event); 874 if (nsEvent.buttonNumber == 2) 875 view._window.handleMouseClicks(nsEvent, MouseButton.middle, false); 876 } 877 878 void otherMouseUp(id self, SEL selector, id event) nothrow @nogc 879 { 880 CocoaScopedCallback scopedCallback; 881 scopedCallback.enter(); 882 883 DPlugCustomView view = getInstance(self); 884 auto nsEvent = NSEvent(event); 885 if (nsEvent.buttonNumber == 2) 886 view._window.handleMouseClicks(nsEvent, MouseButton.middle, true); 887 } 888 889 void mouseMoved(id self, SEL selector, id event) nothrow @nogc 890 { 891 CocoaScopedCallback scopedCallback; 892 scopedCallback.enter(); 893 894 DPlugCustomView view = getInstance(self); 895 view._window.handleMouseMove(NSEvent(event)); 896 } 897 898 void mouseEntered(id self, SEL selector, id event) nothrow @nogc 899 { 900 CocoaScopedCallback scopedCallback; 901 scopedCallback.enter(); 902 DPlugCustomView view = getInstance(self); 903 view._window.handleMouseEntered(NSEvent(event)); 904 } 905 906 void mouseExited(id self, SEL selector, id event) nothrow @nogc 907 { 908 CocoaScopedCallback scopedCallback; 909 scopedCallback.enter(); 910 DPlugCustomView view = getInstance(self); 911 view._window._listener.onMouseExitedWindow(); 912 } 913 914 void updateTrackingAreas(id self, SEL selector) nothrow @nogc 915 { 916 CocoaScopedCallback scopedCallback; 917 scopedCallback.enter(); 918 919 // Call superclass's updateTrackingAreas:, equivalent to [super updateTrackingAreas]; 920 { 921 objc_super sup; 922 sup.receiver = self; 923 sup.clazz = cast(Class) lazyClass!"NSView"; 924 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 925 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 926 } 927 928 DPlugCustomView view = getInstance(self); 929 930 // Remove an existing tracking area, if any. 931 if (view._trackingArea._id !is null) 932 { 933 view.removeTrackingArea(view._trackingArea); 934 view._trackingArea.release(); 935 view._trackingArea._id = null; 936 } 937 938 // This is needed to get mouseEntered and mouseExited 939 int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways); 940 941 NSRect bounds = view.bounds(); 942 view._trackingArea = NSTrackingArea.alloc(); 943 view._trackingArea.initWithRect(bounds, opts, view, null); 944 view.addTrackingArea(view._trackingArea); 945 } 946 947 948 void scrollWheel(id self, SEL selector, id event) nothrow @nogc 949 { 950 CocoaScopedCallback scopedCallback; 951 scopedCallback.enter(); 952 DPlugCustomView view = getInstance(self); 953 view._window.handleMouseWheel(NSEvent(event)); 954 } 955 956 bool acceptsFirstResponder(id self, SEL selector) nothrow @nogc 957 { 958 return YES; 959 } 960 961 bool acceptsFirstMouse(id self, SEL selector, id pEvent) nothrow @nogc 962 { 963 return YES; 964 } 965 966 bool isOpaque(id self, SEL selector) nothrow @nogc 967 { 968 return NO; // Since with the #835 issue, doesn't cover all the dirt rect but only the part intersecting bounds. 969 } 970 971 // Since 10.7, called on resize. 972 void layout(id self, SEL selector) nothrow @nogc 973 { 974 CocoaScopedCallback scopedCallback; 975 scopedCallback.enter(); 976 977 DPlugCustomView view = getInstance(self); 978 view._window.layout(); 979 980 // Call superclass's layout:, equivalent to [super layout]; 981 { 982 objc_super sup; 983 sup.receiver = self; 984 sup.clazz = cast(Class) lazyClass!"NSView"; 985 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 986 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 987 } 988 } 989 990 // Necessary for the Big Sur drawRect: fuckup 991 // See Issue #505. 992 void viewWillDraw(id self, SEL selector) nothrow @nogc 993 { 994 CocoaScopedCallback scopedCallback; 995 scopedCallback.enter(); 996 997 DPlugCustomView view = getInstance(self); 998 view._window.viewWillDraw(); 999 1000 // Call superclass's layout:, equivalent to [super viewWillDraw]; 1001 { 1002 objc_super sup; 1003 sup.receiver = self; 1004 sup.clazz = cast(Class) lazyClass!"NSView"; 1005 alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc; 1006 (cast(fun_t)objc_msgSendSuper)(&sup, selector); 1007 } 1008 } 1009 1010 void viewDidMoveToWindow(id self, SEL selector) nothrow @nogc 1011 { 1012 CocoaScopedCallback scopedCallback; 1013 scopedCallback.enter(); 1014 1015 DPlugCustomView view = getInstance(self); 1016 NSWindow parentWindow = view.window(); 1017 if (parentWindow) 1018 { 1019 parentWindow.makeFirstResponder(view); 1020 parentWindow.setAcceptsMouseMovedEvents(true); 1021 } 1022 } 1023 1024 void drawRect(id self, SEL selector, NSRect rect) nothrow @nogc 1025 { 1026 CocoaScopedCallback scopedCallback; 1027 scopedCallback.enter(); 1028 1029 DPlugCustomView view = getInstance(self); 1030 view._window.drawRect(rect); 1031 } 1032 1033 void onTimer(id self, SEL selector, id timer) nothrow @nogc 1034 { 1035 CocoaScopedCallback scopedCallback; 1036 scopedCallback.enter(); 1037 1038 DPlugCustomView view = getInstance(self); 1039 view._window.onTimer(); 1040 } 1041 }