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 }