1 /**
2 * Cocoa window implementation.
3 * Copyright: Copyright Auburn Sounds 2015 and later.
4 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
5 * Authors:   Guillaume Piolat
6 */
7 module dplug.window.cocoawindow;
8 
9 import core.stdc.stdlib;
10 
11 import std..string;
12 import std.uuid;
13 
14 import gfm.math.vector;
15 import gfm.math.box;
16 import dplug.graphics.image;
17 import dplug.graphics.view;
18 
19 import dplug.core.sync;
20 import dplug.core.runtime;
21 import dplug.core.nogc;
22 import dplug.core.random;
23 import dplug.window.window;
24 
25 import derelict.cocoa;
26 
27 final class CocoaWindow : IWindow
28 {
29 nothrow:
30 @nogc:
31 private:
32     IWindowListener _listener;
33 
34     NSColorSpace _nsColorSpace;
35     CGColorSpaceRef _cgColorSpaceRef;
36     NSData _imageData;
37     NSString _logFormatStr;
38 
39     // Only used by host windows
40     NSWindow _nsWindow;
41     NSApplication _nsApplication;
42 
43     DPlugCustomView _view = null;
44 
45     bool _terminated = false;
46 
47     int _lastMouseX, _lastMouseY;
48     bool _firstMouseMove = true;
49 
50     int _width;
51     int _height;
52 
53     int _askedWidth;
54     int _askedHeight;
55 
56     ImageRef!RGBA _wfb;
57 
58     uint _timeAtCreationInMs;
59     uint _lastMeasturedTimeInMs;
60     bool _dirtyAreasAreNotYetComputed;
61 
62     bool _isHostWindow;
63 
64 public:
65 
66     this(WindowUsage usage, void* parentWindow, IWindowListener listener, int width, int height)
67     {
68         _isHostWindow = (usage == WindowUsage.host);
69 
70         _listener = listener;
71 
72         acquireCocoaFunctions();
73         NSApplicationLoad(); // to use Cocoa in Carbon applications
74         bool parentViewExists = parentWindow !is null;
75 
76         _width = 0;
77         _height = 0;
78 
79         _askedWidth = width;
80         _askedHeight = height;
81 
82         _nsColorSpace = NSColorSpace.sRGBColorSpace();
83         // hopefully not null else the colors will be brighter
84         _cgColorSpaceRef = _nsColorSpace.CGColorSpace();
85 
86         _logFormatStr = NSString.stringWith("%@");
87 
88         _timeAtCreationInMs = getTimeMs();
89         _lastMeasturedTimeInMs = _timeAtCreationInMs;
90 
91         _dirtyAreasAreNotYetComputed = true;
92 
93         if (!_isHostWindow)
94         {
95             DPlugCustomView.generateClassName();
96             DPlugCustomView.registerSubclass();
97 
98             _view = DPlugCustomView.alloc();
99             _view.initialize(this, width, height);
100 
101             // In VST, add the view to the parent view.
102             // In AU (parentWindow == null), a reference to the view is returned instead and the host does it.
103             if (parentWindow !is null)
104             {
105                 NSView parentView = NSView(cast(id)parentWindow);
106                 parentView.addSubview(_view);
107             }
108         }
109         else
110         {
111             _nsApplication = NSApplication.sharedApplication;
112             _nsApplication.setActivationPolicy(NSApplicationActivationPolicyRegular);
113             _nsWindow = NSWindow.alloc();
114             _nsWindow.initWithContentRect(NSMakeRect(0, 0, width, height),
115                                             NSBorderlessWindowMask, NSBackingStoreBuffered, NO);
116             _nsWindow.makeKeyAndOrderFront();
117             _nsApplication.activateIgnoringOtherApps(YES);
118         }
119     }
120 
121     ~this()
122     {
123         if (!_isHostWindow)
124         {
125             _terminated = true;
126 
127             {
128                 _view.killTimer();
129             }
130 
131             _view.removeFromSuperview();
132             _view.release();
133             _view = DPlugCustomView(null);
134 
135             DPlugCustomView.unregisterSubclass();
136         }
137         else
138         {
139             _nsWindow.destroy();
140         }
141 
142         releaseCocoaFunctions();
143     }
144 
145     // Implements IWindow
146     override void waitEventAndDispatch()
147     {
148         if (!_isHostWindow)
149             assert(false); // only valid for a host window
150 
151         NSEvent event = _nsWindow.nextEventMatchingMask(cast(NSUInteger)-1);
152         _nsApplication.sendEvent(event);
153     }
154 
155     override bool terminated()
156     {
157         return _terminated;
158     }
159 
160     override uint getTimeMs()
161     {
162         return cast(uint)(NSDate.timeIntervalSinceReferenceDate() * 1000.0);
163     }
164 
165     override void* systemHandle()
166     {
167         if (_isHostWindow)
168             return _nsWindow.contentView()._id; // return the main NSView
169         else
170             return _view._id;
171     }
172 
173 private:
174 
175     MouseState getMouseState(NSEvent event)
176     {
177         // not working
178         MouseState state;
179         uint pressedMouseButtons = event.pressedMouseButtons();
180         if (pressedMouseButtons & 1)
181             state.leftButtonDown = true;
182         if (pressedMouseButtons & 2)
183             state.rightButtonDown = true;
184         if (pressedMouseButtons & 4)
185             state.middleButtonDown = true;
186 
187         NSEventModifierFlags mod = event.modifierFlags();
188         if (mod & NSControlKeyMask)
189             state.ctrlPressed = true;
190         if (mod & NSShiftKeyMask)
191             state.shiftPressed = true;
192         if (mod & NSAlternateKeyMask)
193             state.altPressed = true;
194 
195         return state;
196     }
197 
198     void handleMouseWheel(NSEvent event)
199     {
200         int deltaX = cast(int)(0.5 + 10 * event.deltaX);
201         int deltaY = cast(int)(0.5 + 10 * event.deltaY);
202         vec2i mousePos = getMouseXY(_view, event, _height);
203         _listener.onMouseWheel(mousePos.x, mousePos.y, deltaX, deltaY, getMouseState(event));
204     }
205 
206     bool handleKeyEvent(NSEvent event, bool released)
207     {
208         uint keyCode = event.keyCode();
209         Key key;
210         switch (keyCode)
211         {
212             case kVK_ANSI_Keypad0: key = Key.digit0; break;
213             case kVK_ANSI_Keypad1: key = Key.digit1; break;
214             case kVK_ANSI_Keypad2: key = Key.digit2; break;
215             case kVK_ANSI_Keypad3: key = Key.digit3; break;
216             case kVK_ANSI_Keypad4: key = Key.digit4; break;
217             case kVK_ANSI_Keypad5: key = Key.digit5; break;
218             case kVK_ANSI_Keypad6: key = Key.digit6; break;
219             case kVK_ANSI_Keypad7: key = Key.digit7; break;
220             case kVK_ANSI_Keypad8: key = Key.digit8; break;
221             case kVK_ANSI_Keypad9: key = Key.digit9; break;
222             case kVK_Return: key = Key.enter; break;
223             case kVK_Escape: key = Key.escape; break;
224             case kVK_LeftArrow: key = Key.leftArrow; break;
225             case kVK_RightArrow: key = Key.rightArrow; break;
226             case kVK_DownArrow: key = Key.downArrow; break;
227             case kVK_UpArrow: key = Key.upArrow; break;
228             case kVK_Delete: key = Key.backspace; break;
229             default:
230             {
231                 NSString characters = event.charactersIgnoringModifiers();
232                 if (characters.length() == 0)
233                 {
234                     key = Key.unsupported;
235                 }
236                 else
237                 {
238                     wchar ch = characters.characterAtIndex(0);
239                     if (ch >= '0' && ch <= '9')
240                         key = cast(Key)(Key.digit0 + (ch - '0'));
241                     else if (ch >= 'A' && ch <= 'Z')
242                         key = cast(Key)(Key.A + (ch - 'A'));
243                     else if (ch >= 'a' && ch <= 'z')
244                         key = cast(Key)(Key.a + (ch - 'a'));
245                     else
246                         key = Key.unsupported;
247                 }
248             }
249         }
250 
251         bool handled = false;
252 
253         if (released)
254         {
255             if (_listener.onKeyDown(key))
256                 handled = true;
257         }
258         else
259         {
260             if (_listener.onKeyUp(key))
261                 handled = true;
262         }
263 
264         return handled;
265     }
266 
267     void handleMouseMove(NSEvent event)
268     {
269         vec2i mousePos = getMouseXY(_view, event, _height);
270 
271         if (_firstMouseMove)
272         {
273             _firstMouseMove = false;
274             _lastMouseX = mousePos.x;
275             _lastMouseY = mousePos.y;
276         }
277 
278         _listener.onMouseMove(mousePos.x, mousePos.y, mousePos.x - _lastMouseX, mousePos.y - _lastMouseY,
279             getMouseState(event));
280 
281         _lastMouseX = mousePos.x;
282         _lastMouseY = mousePos.y;
283     }
284 
285     void handleMouseClicks(NSEvent event, MouseButton mb, bool released)
286     {
287         vec2i mousePos = getMouseXY(_view, event, _height);
288 
289         if (released)
290             _listener.onMouseRelease(mousePos.x, mousePos.y, mb, getMouseState(event));
291         else
292         {
293             int clickCount = event.clickCount();
294             bool isDoubleClick = clickCount >= 2;
295             _listener.onMouseClick(mousePos.x, mousePos.y, mb, isDoubleClick, getMouseState(event));
296         }
297     }
298 
299     enum scanLineAlignment = 4; // could be anything
300 
301     // given a width, how long in bytes should scanlines be
302     int byteStride(int width)
303     {
304         int widthInBytes = width * 4;
305         return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1);
306     }
307 
308     void drawRect(NSRect rect)
309     {
310         NSGraphicsContext nsContext = NSGraphicsContext.currentContext();
311 
312         CIContext ciContext = nsContext.getCIContext();
313 
314         // Updates internal buffers in case of startup/resize
315         // FUTURE: why is the bounds rect too large? It creates havoc in AU even without resizing.
316         {
317             /*
318             NSRect boundsRect = _view.bounds();
319             int width = cast(int)(boundsRect.size.width);   // truncating down the dimensions of bounds
320             int height = cast(int)(boundsRect.size.height);
321             */
322             updateSizeIfNeeded(_askedWidth, _askedHeight);
323         }
324 
325         // The first drawRect callback occurs before the timer triggers.
326         // But because recomputeDirtyAreas() wasn't called before there is nothing to draw.
327         // Hence, do it.
328         if (_dirtyAreasAreNotYetComputed)
329         {
330             _dirtyAreasAreNotYetComputed = false;
331             _listener.recomputeDirtyAreas();
332         }
333 
334         _listener.onDraw(WindowPixelFormat.ARGB8);
335 
336         size_t sizeNeeded = _wfb.pitch * _wfb.h;
337         _imageData = NSData.dataWithBytesNoCopy(_wfb.pixels, sizeNeeded, false);
338 
339         CIImage image = CIImage.imageWithBitmapData(_imageData,
340                                                     byteStride(_width),
341                                                     CGSize(_width, _height),
342                                                     kCIFormatARGB8,
343                                                     _cgColorSpaceRef);
344 
345         ciContext.drawImage(image, rect, rect);
346     }
347 
348     /// Returns: true if window size changed.
349     bool updateSizeIfNeeded(int newWidth, int newHeight)
350     {
351         // only do something if the client size has changed
352         if ( (newWidth != _width) || (newHeight != _height) )
353         {
354             _width = newWidth;
355             _height = newHeight;
356             _wfb = _listener.onResized(_width, _height);
357             return true;
358         }
359         else
360             return false;
361     }
362 
363     void doAnimation()
364     {
365         uint now = getTimeMs();
366         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
367         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
368         _lastMeasturedTimeInMs = now;
369         _listener.onAnimate(dt, time);
370     }
371 
372     void onTimer()
373     {
374         // Deal with animation
375         doAnimation();
376 
377         _listener.recomputeDirtyAreas();
378         _dirtyAreasAreNotYetComputed = false;
379         box2i dirtyRect = _listener.getDirtyRectangle();
380         if (!dirtyRect.empty())
381         {
382 
383             NSRect boundsRect = _view.bounds();
384             int height = cast(int)(boundsRect.size.height);
385             NSRect r = NSMakeRect(dirtyRect.min.x,
386                                     height - dirtyRect.min.y - dirtyRect.height,
387                                     dirtyRect.width,
388                                     dirtyRect.height);
389             _view.setNeedsDisplayInRect(r);
390         }
391     }
392 }
393 
394 struct DPlugCustomView
395 {
396 public:
397 nothrow:
398 @nogc:
399 
400     NSView parent;
401     alias parent this;
402 
403     // create from an id
404     this (id id_)
405     {
406         this._id = id_;
407     }
408 
409     /// Allocates, but do not init
410     static DPlugCustomView alloc()
411     {
412         alias fun_t = extern(C) id function (id obj, SEL sel) nothrow @nogc;
413         return DPlugCustomView( (cast(fun_t)objc_msgSend)(getClassID(), sel!"alloc") );
414     }
415 
416     static Class getClass()
417     {
418         return cast(Class)( getClassID() );
419     }
420 
421     static id getClassID()
422     {
423         return objc_getClass(customClassName.ptr);
424     }
425 
426     // This class uses a unique class name for each plugin instance
427     static __gshared char[16 + 36 + 1] customClassName;
428 
429     static void generateClassName() nothrow @nogc
430     {
431         generateNullTerminatedRandomUUID!char(customClassName, "DPlugCustomView_");
432     }
433 
434 private:
435 
436     CocoaWindow _window;
437     NSTimer _timer = null;
438     NSString _runLoopMode;
439 
440     void initialize(CocoaWindow window, int width, int height)
441     {
442         // Warning: taking this address is fishy since DPlugCustomView is a struct and thus could be copied
443         // we rely on the fact it won't :|
444         void* thisPointer = cast(void*)(&this);
445         object_setInstanceVariable(_id, "this", thisPointer);
446 
447         this._window = window;
448 
449         NSRect r = NSRect(NSPoint(0, 0), NSSize(width, height));
450         initWithFrame(r);
451 
452         _timer = NSTimer.timerWithTimeInterval(1 / 60.0, this, sel!"onTimer:", null, true);
453         _runLoopMode = NSString.stringWith("kCFRunLoopCommonModes"w);
454         NSRunLoop.currentRunLoop().addTimer(_timer, _runLoopMode);
455     }
456 
457     static __gshared Class clazz;
458 
459     static void registerSubclass()
460     {
461         clazz = objc_allocateClassPair(cast(Class) lazyClass!"NSView", customClassName.ptr, 0);
462 
463         class_addMethod(clazz, sel!"keyDown:", cast(IMP) &keyDown, "v@:@");
464         class_addMethod(clazz, sel!"keyUp:", cast(IMP) &keyUp, "v@:@");
465         class_addMethod(clazz, sel!"mouseDown:", cast(IMP) &mouseDown, "v@:@");
466         class_addMethod(clazz, sel!"mouseUp:", cast(IMP) &mouseUp, "v@:@");
467         class_addMethod(clazz, sel!"rightMouseDown:", cast(IMP) &rightMouseDown, "v@:@");
468         class_addMethod(clazz, sel!"rightMouseUp:", cast(IMP) &rightMouseUp, "v@:@");
469         class_addMethod(clazz, sel!"otherMouseDown:", cast(IMP) &otherMouseDown, "v@:@");
470         class_addMethod(clazz, sel!"otherMouseUp:", cast(IMP) &otherMouseUp, "v@:@");
471         class_addMethod(clazz, sel!"mouseMoved:", cast(IMP) &mouseMoved, "v@:@");
472         class_addMethod(clazz, sel!"mouseDragged:", cast(IMP) &mouseMoved, "v@:@");
473         class_addMethod(clazz, sel!"rightMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
474         class_addMethod(clazz, sel!"otherMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
475         class_addMethod(clazz, sel!"acceptsFirstResponder", cast(IMP) &acceptsFirstResponder, "b@:");
476         class_addMethod(clazz, sel!"isOpaque", cast(IMP) &isOpaque, "b@:");
477         class_addMethod(clazz, sel!"acceptsFirstMouse:", cast(IMP) &acceptsFirstMouse, "b@:@");
478         class_addMethod(clazz, sel!"viewDidMoveToWindow", cast(IMP) &viewDidMoveToWindow, "v@:");
479         class_addMethod(clazz, sel!"drawRect:", cast(IMP) &drawRect, "v@:" ~ encode!NSRect);
480         class_addMethod(clazz, sel!"onTimer:", cast(IMP) &onTimer, "v@:@");
481 
482         class_addMethod(clazz, sel!"mouseEntered:", cast(IMP) &mouseEntered, "v@:@");
483         class_addMethod(clazz, sel!"mouseExited:", cast(IMP) &mouseExited, "v@:@");
484 
485         // This ~ is to avoid a strange DMD ICE. Didn't succeed in isolating it.
486         class_addMethod(clazz, sel!("scroll" ~ "Wheel:") , cast(IMP) &scrollWheel, "v@:@");
487 
488         // very important: add an instance variable for the this pointer so that the D object can be
489         // retrieved from an id
490         class_addIvar(clazz, "this", (void*).sizeof, (void*).sizeof == 4 ? 2 : 3, "^v");
491 
492         objc_registerClassPair(clazz);
493     }
494 
495     static void unregisterSubclass()
496     {
497         // For some reason the class need to continue to exist, so we leak it
498         //  objc_disposeClassPair(clazz);
499         // TODO: remove this crap
500     }
501 
502     void killTimer()
503     {
504         if (_timer)
505         {
506             _timer.invalidate();
507             _timer = NSTimer(null);
508         }
509     }
510 }
511 
512 DPlugCustomView getInstance(id anId) nothrow @nogc
513 {
514     // strange thing: object_getInstanceVariable definition is odd (void**)
515     // and only works for pointer-sized values says SO
516     void* thisPointer = null;
517     Ivar var = object_getInstanceVariable(anId, "this", &thisPointer);
518     assert(var !is null);
519     assert(thisPointer !is null);
520     return *cast(DPlugCustomView*)thisPointer;
521 }
522 
523 vec2i getMouseXY(NSView view, NSEvent event, int windowHeight) nothrow @nogc
524 {
525     NSPoint mouseLocation = event.locationInWindow();
526     mouseLocation = view.convertPoint(mouseLocation, NSView(null));
527     int px = cast(int)(mouseLocation.x) - 2;
528     int py = windowHeight - cast(int)(mouseLocation.y) - 3;
529     return vec2i(px, py);
530 }
531 
532 
533 
534 alias CocoaScopedCallback = ScopedForeignCallback!(true, true);
535 
536 // Overridden function gets called with an id, instead of the self pointer.
537 // So we have to get back the D class object address.
538 // Big thanks to Mike Ash (@macdev)
539 // MAYDO: why are these methods members???
540 extern(C)
541 {
542     void keyDown(id self, SEL selector, id event) nothrow @nogc
543     {
544         CocoaScopedCallback scopedCallback;
545         scopedCallback.enter();
546 
547         DPlugCustomView view = getInstance(self);
548         bool handled = view._window.handleKeyEvent(NSEvent(event), false);
549 
550         // send event to superclass if event not handled
551         if (!handled)
552         {
553             objc_super sup;
554             sup.receiver = self;
555             sup.clazz = cast(Class) lazyClass!"NSView";
556             alias fun_t = extern(C) void function (objc_super*, SEL, id) nothrow @nogc;
557             (cast(fun_t)objc_msgSendSuper)(&sup, selector, event);
558         }
559     }
560 
561     void keyUp(id self, SEL selector, id event) nothrow @nogc
562     {
563         CocoaScopedCallback scopedCallback;
564         scopedCallback.enter();
565 
566         DPlugCustomView view = getInstance(self);
567         view._window.handleKeyEvent(NSEvent(event), true);
568     }
569 
570     void mouseDown(id self, SEL selector, id event) nothrow @nogc
571     {
572         CocoaScopedCallback scopedCallback;
573         scopedCallback.enter();
574 
575         DPlugCustomView view = getInstance(self);
576         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, false);
577     }
578 
579     void mouseUp(id self, SEL selector, id event) nothrow @nogc
580     {
581         CocoaScopedCallback scopedCallback;
582         scopedCallback.enter();
583 
584         DPlugCustomView view = getInstance(self);
585         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, true);
586     }
587 
588     void rightMouseDown(id self, SEL selector, id event) nothrow @nogc
589     {
590         CocoaScopedCallback scopedCallback;
591         scopedCallback.enter();
592 
593         DPlugCustomView view = getInstance(self);
594         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, false);
595     }
596 
597     void rightMouseUp(id self, SEL selector, id event) nothrow @nogc
598     {
599         CocoaScopedCallback scopedCallback;
600         scopedCallback.enter();
601 
602         DPlugCustomView view = getInstance(self);
603         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, true);
604     }
605 
606     void otherMouseDown(id self, SEL selector, id event) nothrow @nogc
607     {
608         CocoaScopedCallback scopedCallback;
609         scopedCallback.enter();
610 
611         DPlugCustomView view = getInstance(self);
612         auto nsEvent = NSEvent(event);
613         if (nsEvent.buttonNumber == 2)
614             view._window.handleMouseClicks(nsEvent, MouseButton.middle, false);
615     }
616 
617     void otherMouseUp(id self, SEL selector, id event) nothrow @nogc
618     {
619         CocoaScopedCallback scopedCallback;
620         scopedCallback.enter();
621 
622         DPlugCustomView view = getInstance(self);
623         auto nsEvent = NSEvent(event);
624         if (nsEvent.buttonNumber == 2)
625             view._window.handleMouseClicks(nsEvent, MouseButton.middle, true);
626     }
627 
628     void mouseMoved(id self, SEL selector, id event) nothrow @nogc
629     {
630         CocoaScopedCallback scopedCallback;
631         scopedCallback.enter();
632 
633         DPlugCustomView view = getInstance(self);
634         view._window.handleMouseMove(NSEvent(event));
635     }
636 
637     void mouseEntered(id self, SEL selector, id event) nothrow @nogc
638     {
639         CocoaScopedCallback scopedCallback;
640         scopedCallback.enter();
641         NSCursor.arrowCursor().push();
642     }
643 
644     void mouseExited(id self, SEL selector, id event) nothrow @nogc
645     {
646         CocoaScopedCallback scopedCallback;
647         scopedCallback.enter();
648         NSCursor.pop();
649     }
650 
651 
652     void scrollWheel(id self, SEL selector, id event) nothrow @nogc
653     {
654         CocoaScopedCallback scopedCallback;
655         scopedCallback.enter();
656         DPlugCustomView view = getInstance(self);
657         view._window.handleMouseWheel(NSEvent(event));
658     }
659 
660     bool acceptsFirstResponder(id self, SEL selector) nothrow @nogc
661     {
662         return YES;
663     }
664 
665     bool acceptsFirstMouse(id self, SEL selector, id pEvent) nothrow @nogc
666     {
667         return YES;
668     }
669 
670     bool isOpaque(id self, SEL selector) nothrow @nogc
671     {
672         return YES;
673     }
674 
675     void viewDidMoveToWindow(id self, SEL selector) nothrow @nogc
676     {
677         CocoaScopedCallback scopedCallback;
678         scopedCallback.enter();
679 
680         DPlugCustomView view = getInstance(self);
681         NSWindow parentWindow = view.window();
682         if (parentWindow)
683         {
684             parentWindow.makeFirstResponder(view);
685             parentWindow.setAcceptsMouseMovedEvents(true);
686         }
687     }
688 
689     void drawRect(id self, SEL selector, NSRect rect) nothrow @nogc
690     {
691         CocoaScopedCallback scopedCallback;
692         scopedCallback.enter();
693 
694         DPlugCustomView view = getInstance(self);
695         view._window.drawRect(rect);
696     }
697 
698     void onTimer(id self, SEL selector, id timer) nothrow @nogc
699     {
700         CocoaScopedCallback scopedCallback;
701         scopedCallback.enter();
702 
703         DPlugCustomView view = getInstance(self);
704         view._window.onTimer();
705     }
706 }